Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,23 @@ build:
GOOS=$$GOOS GOARCH=$$GOARCH go build -ldflags "-X $(ENV_PKG).Version=$(VERSION) -X $(ENV_PKG).CommitHash=$(COMMIT_HASH) -X $(ENV_PKG).BuildTime=$(BUILD_TIME)" -o $(OUTPUT_DIR)/$$output_name $(MAIN_FILE); \
done

# Default plugin source folder
PLUGIN_SRC ?= plugins
# Output folder for compiled plugins
PLUGIN_OUT ?= bin/plugins

# Ensure output folder exists before building
$(PLUGIN_OUT):
mkdir -p $(PLUGIN_OUT)

plugins: $(PLUGIN_OUT)
@echo "Building plugins from folder: $(PLUGIN_SRC) into $(PLUGIN_OUT)"
@for f in $(PLUGIN_SRC)/*.go; do \
plugin_name=$$(basename $$f .go); \
echo "Building plugin $$plugin_name.so"; \
go build -buildmode=plugin -o $(PLUGIN_OUT)/$$plugin_name.so $$f; \
done

clean:
rm -rf $(OUTPUT_DIR)

Expand Down
40 changes: 39 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,44 @@ To see the complete list of supported formats, run:
foo@bar$ digler formats
```

## Adding Custom Scanners via Plugins

Digler supports a plugin architecture that allows you to extend the tool with custom file scanners. This makes it easy to add support for new file formats or specialized carving logic without modifying the core code.

### Plugin Interface
Your plugin must implement the following interface:

```golang
type FileScanner interface {
Ext() string // Returns the file extension this scanner handles
Description() string // A brief description of the file type
Signatures() [][]byte // Byte signatures used to identify the file type
ScanFile(r *Reader) (*ScanResult, error) // Logic to scan and recover files from a Reader
}
```

When your plugin is ready, place its source file in the plugins/ directory and compile all plugins by running:

```bash
foo@bar$ make plugins
```

This will build your plugin(s) as `.so` files and place them in the **bin/plugins/** folder, ready to be loaded by Digler.

To verify that your plugins are correctly loaded, run:

```bash
foo@bar$ digler formats --plugins ./bin/plugins
```

This command will lists all supported file formats, including those provided by your custom plugins.

Finally, use the same --plugins flag when scanning to enable your plugins:

```bash
foo@bar$ digler scan <image_or_device> --plugins ./bin/plugins
```

## Contributing

Writing a comprehensive file carver is a complex challenge. Each supported file type often requires a format-specific decoder to properly identify, validate, and reconstruct data. This makes the development of Digler both technically demanding and highly modular — the perfect scenario for open source collaboration.
Expand All @@ -150,6 +188,6 @@ Before you start, **please open an issue** (or pick an existing one) to discuss

Once you're ready, fork the repository and submit your pull request!

### License
## License

Digler is released under the **MIT License**.
29 changes: 22 additions & 7 deletions cmd/cmd/formats.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import (
)

func DefineFormatsCommand() *cobra.Command {
return &cobra.Command{
cmd := &cobra.Command{
Use: "formats",
Short: "List all supported file formats",
Long: `The 'formats' command displays a table of all file formats currently supported by the recovery scanner.
Expand All @@ -40,26 +40,41 @@ Each format includes its name, associated file extensions, category (e.g., image
SilenceUsage: true,
RunE: RunFormats,
}

cmd.Flags().StringSlice("plugins", nil, "paths to plugin .so files or directories containing plugins")
return cmd
}

func RunFormats(cmd *cobra.Command, args []string) error {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "NAME\tDESC\tSIGNATURES")

headers, err := format.FileHeaders()
scanners, err := format.GetFileScanners()
if err != nil {
return err
}

plugins, _ := cmd.Flags().GetStringSlice("plugins")
pluginPaths, err := listPlugins(plugins)
if err != nil {
return err
}

for _, hdr := range headers {
signatures := make([]string, len(hdr.Signatures))
for i, sig := range hdr.Signatures {
pluginScanners, err := format.LoadPlugins(pluginPaths...)
if err != nil {
return fmt.Errorf("failed to load plugins: %w", err)
}
scanners = append(scanners, pluginScanners...)

for _, sc := range scanners {
signatures := make([]string, len(sc.Signatures()))
for i, sig := range sc.Signatures() {
signatures[i] = hex.EncodeToString(sig)
}

fmt.Fprintf(w, "%s\t%s\t%s\n",
hdr.Ext,
hdr.Description,
sc.Ext(),
sc.Description(),
strings.Join(signatures, ","),
)
}
Expand Down
59 changes: 56 additions & 3 deletions cmd/cmd/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@
package cmd

import (
"fmt"
"io/fs"
"math"
"os"
"path/filepath"
"strings"

"github.com/ostafen/digler/internal/disk"
"github.com/ostafen/digler/internal/logger"
Expand All @@ -46,17 +51,22 @@ func DefineScanCommand() *cobra.Command {
cmd.Flags().Bool("no-log", false, "disable logging")
cmd.Flags().StringSliceP("ext", "", nil, "file extensions to parse")
cmd.Flags().StringP("output", "o", "", "The path of the scan index file")
cmd.Flags().StringSlice("plugins", nil, "paths to plugin .so files or directories containing plugins")

return cmd
}

func RunScan(cmd *cobra.Command, args []string) error {
path := disk.NormalizeVolumePath(args[0])
opts := parseOptions(cmd)

opts, err := parseOptions(cmd)
if err != nil {
return err
}
return scan.Scan(path, opts)
}

func parseOptions(cmd *cobra.Command) scan.Options {
func parseOptions(cmd *cobra.Command) (scan.Options, error) {
dumpDir := cmd.Flag("dump").Value.String()
disableLog, _ := cmd.Flags().GetBool("no-log")
outputFile, _ := cmd.Flags().GetString("output")
Expand All @@ -69,6 +79,13 @@ func parseOptions(cmd *cobra.Command) scan.Options {
fileExt, _ := cmd.Flags().GetStringSlice("ext")
logLevel, _ := cmd.Flags().GetString("log-level")

plugins, _ := cmd.Flags().GetStringSlice("plugins")

pluginPaths, err := listPlugins(plugins)
if err != nil {
return scan.Options{}, nil
}

return scan.Options{
DumpDir: dumpDir,
ReportFile: outputFile,
Expand All @@ -78,8 +95,9 @@ func parseOptions(cmd *cobra.Command) scan.Options {
MaxFileSize: maxFileSize,
DisableLog: disableLog,
FileExt: fileExt,
Plugins: pluginPaths,
LogLevel: logger.ParseLevel(logLevel),
}
}, nil
}

func getBytes(cmd *cobra.Command, name string) uint64 {
Expand All @@ -91,3 +109,38 @@ func getBytes(cmd *cobra.Command, name string) uint64 {
}
return v
}

// listPlugins expands plugin paths: if path is a file, add it directly;
// if path is a directory, scan it recursively for .so files.
func listPlugins(plugins []string) ([]string, error) {
var pluginPaths []string

for _, p := range plugins {
info, err := os.Stat(p)
if err != nil {
return nil, err
}

if !info.IsDir() {
if !strings.HasSuffix(info.Name(), ".so") {
return nil, fmt.Errorf("plugin file %s does not have .so extension", info.Name())
}
pluginPaths = append(pluginPaths, p)
continue
}

err = filepath.WalkDir(p, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() && strings.HasSuffix(d.Name(), ".so") {
pluginPaths = append(pluginPaths, path)
}
return nil
})
if err != nil {
return nil, err
}
}
return pluginPaths, nil
}
47 changes: 47 additions & 0 deletions internal/format/file_scanner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) 2025 Stefano Scafiti
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package format

type FileScanner interface {
Ext() string
Description() string
Signatures() [][]byte
ScanFile(r *Reader) (*ScanResult, error)
}

type headerFileScanner struct {
hdr FileHeader
}

func (s *headerFileScanner) Ext() string {
return s.hdr.Ext
}

func (s *headerFileScanner) Description() string {
return s.hdr.Description
}

func (s *headerFileScanner) Signatures() [][]byte {
return s.hdr.Signatures
}

func (s *headerFileScanner) ScanFile(r *Reader) (*ScanResult, error) {
return s.hdr.ScanFile(r)
}
62 changes: 53 additions & 9 deletions internal/format/header.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@
// THE SOFTWARE.
package format

import "fmt"
import (
"fmt"
"plugin"
"reflect"
)

type ScanResult struct {
Name string
Expand Down Expand Up @@ -52,35 +56,75 @@ var fileHeaders = []FileHeader{
pdfFileHeader,
}

func FileHeaders(ext ...string) ([]FileHeader, error) {
func GetFileScanners(ext ...string) ([]FileScanner, error) {
if len(ext) == 0 {
return fileHeaders, nil
scanners := GetAllFileScanners()
return scanners, nil
}

headersByExt := make(map[string]FileHeader)
for _, hdr := range fileHeaders {
headersByExt[hdr.Ext] = hdr
}

headers := make([]FileHeader, len(ext))
scanners := make([]FileScanner, len(ext))
for i, e := range ext {
hdr, ok := headersByExt[e]
if !ok {
return nil, fmt.Errorf("unknown file extension: \"%s\"", hdr.Ext)
}
headers[i] = hdr
scanners[i] = &headerFileScanner{hdr: hdr}
}
return headers, nil
return scanners, nil
}

func BuildFileRegistry(headers ...FileHeader) *FileRegistry {
func GetAllFileScanners() []FileScanner {
scanners := make([]FileScanner, len(fileHeaders))
for i, hdr := range fileHeaders {
scanners[i] = &headerFileScanner{hdr: hdr}
}
return scanners
}

func BuildFileRegistry(scanners ...FileScanner) *FileRegistry {
r := NewFileRegisty()
for _, hdr := range headers {
r.Add(hdr)
for _, sc := range scanners {
r.Add(sc)
}
return r
}

func LoadPlugins(pluginPaths ...string) ([]FileScanner, error) {
scanners := make([]FileScanner, len(pluginPaths))
for i, path := range pluginPaths {
sc, err := loadPlugin(path)
if err != nil {
return nil, fmt.Errorf("failed to load plugin %s: %w", path, err)
}
scanners[i] = sc
}
return scanners, nil
}

func loadPlugin(path string) (FileScanner, error) {
plug, err := plugin.Open(path)
if err != nil {
return nil, fmt.Errorf("failed to open plugin %s: %w", path, err)
}

symScanner, err := plug.Lookup("GetScanner")
if err != nil {
return nil, fmt.Errorf("plugin %s does not export FileScanner symbol: %w", path, err)
}

fmt.Println(reflect.TypeOf(symScanner))
getScanner, ok := symScanner.(func() (FileScanner, error))
if !ok {
return nil, fmt.Errorf("plugin %s GetScanner has wrong type", path)
}
return getScanner()
}

func (r *FileRegistry) Signatures() int {
return r.table.Size()
}
Loading
Loading