Compare commits

..

10 Commits

Author SHA1 Message Date
Grigoryev Ilya Alekseevich
03f2eeef09 add validation 2026-04-01 00:50:06 +05:00
Grigoryev Ilya Alekseevich
6745820f71 fix README 2026-04-01 00:18:17 +05:00
Grigoryev Ilya Alekseevich
a2b8c67064 fix README 2026-04-01 00:13:41 +05:00
Grigoryev Ilya Alekseevich
646e89dbee update gitignore 2026-04-01 00:11:46 +05:00
Grigoryev Ilya Alekseevich
abd754e22d rebase 2026-04-01 00:10:37 +05:00
Grigoryev Ilya Alekseevich
5d5dc0d33a add README 2026-04-01 00:05:59 +05:00
Grigoryev Ilya Alekseevich
9af8091edd fix structure error 2026-03-31 23:34:37 +05:00
Grigoryev Ilya Alekseevich
b8997e1ac9 refactor 2026-03-31 23:22:50 +05:00
Grigoryev Ilya Alekseevich
9df60aa41a rework args 2026-03-31 21:56:55 +05:00
Grigoryev Ilya Alekseevich
c28cb8d48a init commit 2026-03-31 01:08:22 +05:00
12 changed files with 528 additions and 3 deletions

2
.gitignore vendored
View File

@@ -24,4 +24,4 @@ go.work.sum
# env file
.env
.DS_Store

View File

@@ -1,3 +1,61 @@
# MFTECmd-for-Linux
# MFTECmd
Переработка утилиты MFTECmd, разработанной Эриком Циммерманом под работу на *nix
[![Golang](https://img.shields.io/badge/Golang-Go-00ADD8?logo=go&logoColor=white)](https://go.dev/) [![Source](https://img.shields.io/badge/Source-GitHub-181717?logo=github&logoColor=white)](https://github.com/EricZimmerman/MFTECmd)
Утилита является портированной под Linux версией утилиты [Эрика Циммермана](https://github.com/EricZimmerman). При портировании была пересмотрена структура флагов для более простого взаимодействия с программой.
---
## Поддерживаемые форматы файлов
В качестве входных файлов утилита поддерживает:
- `$MFT` - системный файл в файловой системе NTFS, в котором хранится информация о содержимом тома.
- `$J` (USN Journal) - системный файл в файловой системе NTFS, который хранит журнал изменений на томе.
- `$Boot` - системный файл в файловой системе NTFS, содержащий загрузочный сектор и код запуска.
- `$SDS` - системный файл в файловой системе NTFS, содержит список дескрипторов безопасности для всех файлов и каталогов на томе.
- `$I30` - атрибут индекса размещения. Связан с каталогами и содержит информацию о файлах и подкаталогах, содержащихся в каталоге.
- `$LogFile` - системный файл в файловой системе NTFS, содержащий журнал транзакций.
Для экспорта доступны форматы: CSV, JSON и Bodyfile.
### CLI
```sh
Usage: mftecmd <command> [flags]
Utility for processing $MFT, $J, $LogFile, $Boot, $SDS, $I30
Flags:
-h, --help Show context-sensitive help.
--debug Show debug information during processing
--trace Show trace information during processing
Commands:
mft parse <path> [flags]
Parse $MFT and write CSV, JSON, or bodyfile output
mft show <path> <entry> [flags]
Show full details for a specific $MFT entry or entry-sequence
mft export-record --offset=STRING --out=STRING <path>
Export one raw $MFT FILE record by offset
j parse <path> [flags]
Parse $J and write CSV or JSON output
boot parse <path> [flags]
Parse $Boot. CSV output is optional, matching MFTECmd behavior
sds parse <path> [flags]
Parse $SDS and write CSV output
sds show <path> <security-id>
Show full details for a specific Security ID from $SDS
i30 parse <path> [flags]
Parse $I30 and write CSV output
logfile parse <path>
Recognize $LogFile input, but report that parsing is not supported yet
```
## Документация
[Introducing MFTECmd](https://binaryforay.blogspot.com/2018/06/introducing-mftecmd.html)

14
app/main.go Normal file
View File

@@ -0,0 +1,14 @@
package main
import (
"fmt"
"mftecmd/internal/cmd"
)
func main() {
program := cmd.NewCLI()
parsed := program.Run()
fmt.Printf("%+v\n", parsed)
fmt.Printf("%#v\n", parsed)
}

8
go.mod Normal file
View File

@@ -0,0 +1,8 @@
module mftecmd
go 1.25.3
require (
github.com/alecthomas/kong v1.14.0 // indirect
github.com/fred1268/go-clap v1.2.1 // indirect
)

4
go.sum Normal file
View File

@@ -0,0 +1,4 @@
github.com/alecthomas/kong v1.14.0 h1:gFgEUZWu2ZmZ+UhyZ1bDhuutbKN1nTtJTwh19Wsn21s=
github.com/alecthomas/kong v1.14.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I=
github.com/fred1268/go-clap v1.2.1 h1:wi8Tokb2zmOEuwwTTfKX5Sj1h6ZpT2BxRtx1/ZJsol4=
github.com/fred1268/go-clap v1.2.1/go.mod h1:A5/yYBapOy6UyujlbxL7p/bX9J7bzyoMRzQKFwveXF0=

BIN
internal/.DS_Store vendored Normal file

Binary file not shown.

178
internal/cmd/args.go Normal file
View File

@@ -0,0 +1,178 @@
// Пакет для работы с CLI интерфейсом
package cmd
// Константы для значений по умолчанию
const (
DefaultTimeFormat = "yyyy-MM-dd HH:mm:ss.fffffff" // Формат временных меток
DefaultResidentMax = 1024 // Ограничение размера резидентных файлов
MaxResidentDataBytes = 1024000 // Максимальное ограничение размера резидентных файлов
)
// Корневая структура флагов.
// Используется для выбора типа файлов
type CMD struct {
Debug bool `help:"Show debug information during processing"`
Trace bool `help:"Show trace information during processing"`
MFT MFTGroup `name:"mft" cmd:"" help:"Processing with $MFT"`
J JournalGroup `name:"j" cmd:"" help:"Processing with $J (USN Journal)"`
Boot BootGroup `name:"boot" cmd:"" help:"Processing with $Boot"`
SDS SDSGroup `name:"sds" cmd:"" help:"Processing with $SDS"`
I30 I30Group `name:"i30" cmd:"" help:"Processing with $I30"`
LogFile LogFileGroup `name:"logfile" cmd:"" help:"Processing with $LogFile"`
}
// Операции с $MFT
type MFTGroup struct {
Parse MFTParseCmd `name:"parse" cmd:"" help:"Parse $MFT and write CSV, JSON, or bodyfile output"` // Парсинг $MFT записей
Show MFTShowCmd `name:"show" cmd:"" help:"Show full details for a specific $MFT entry or entry-sequence"` // Вывод информации о сущностях или последовательностях сущностей
ExportRecord MFTExportRecordCmd `name:"export-record" cmd:"" help:"Export one raw $MFT FILE record by offset"` // Извлечение конкретной $MFT записи
}
// Операции с $J (USN Journal)
type JournalGroup struct {
Parse JournalParseCmd `name:"parse" cmd:"" help:"Parse $J and write CSV or JSON output"` // Парсинг $J записей
}
// Операции с $Boot
type BootGroup struct {
Parse BootParseCmd `name:"parse" cmd:"" help:"Parse $Boot. CSV output is optional, matching MFTECmd behavior"` // Парсинг $Boot записей
}
// Операции с $SDS
type SDSGroup struct {
Parse SDSParseCmd `name:"parse" cmd:"" help:"Parse $SDS and write CSV output"` // Парсинг $SDS записей
Show SDSShowCmd `name:"show" cmd:"" help:"Show full details for a specific Security ID from $SDS"` // Вывод информации по конкретному идентификатору из $SDS
}
// Операции с $I30
type I30Group struct {
Parse I30ParseCmd `name:"parse" cmd:"" help:"Parse $I30 and write CSV output"` // Парсинг $I30 записей
}
// Операции с $LogFile
type LogFileGroup struct {
Parse LogFileParseCmd `name:"parse" cmd:"" help:"Recognize $LogFile input, but report that parsing is not supported yet"` // Парсинг $LogFile записей
}
// Переиспользуемые блоки флагов.
// Сохранение резульатов в CSV
type CSVOutput struct {
CSVPath string `name:"csv" short:"c" help:"Path to save CSV formatted results to" type:"path"` // Путь сохранения файла
}
// Сохранение резульатов в JSON
type JSONOutput struct {
JSONPath string `name:"json" short:"j" help:"Path to save CSV formatted results to" type:"path"` // Путь сохранения файла
}
// Сохранение резульатов в Bodyfile
type BodyOutput struct {
BodyPath string `name:"body" short:"b" help:"Path to save bodyfile formatted results to" type:"path"` // Путь сохранения файла
DriveLetter string `name:"drive-letter" short:"l" help:"Drive letter (C, D, etc.) to use with bodyfile. Only the drive letter itself should be provided"` //Буква диска
UseLF bool `name:"line-feed" default:"false" help:"When true, use LF vs CRLF for newlines. Default is FALSE"` // Окончание строки. false - CRLF, true - LF
}
// Флаги для работы с VSS
type VSSOptions struct {
VSS bool `name:"vss" default:"false" help:"Process all Volume Shadow Copies that exist on drive. Default is FALSE"` // Включает вывод VSC для диска
Dedupe bool `name:"dedupe" default:"false" help:"Deduplicate VSCs based on SHA-1. First file found wins. Default is FALSE"` // Включает удаление дубликатов VSS
}
// Флаги для работы с временными метками
type TimeFormatOption struct {
TimeFormat string `name:"time-format" short:"t" default:"yyyy-MM-dd HH:mm:ss.fffffff" help:"The custom date/time format to use when displaying time stamps. Default is: yyyy-MM-dd HH:mm:ss.fffffff"` // Задаёт формат временных меток
}
// Флаши для работы с резидентными файлами
type ResidentDataOptions struct {
DumpResidentFiles bool `name:"dump-resident-files" default:"false" help:"Dump resident files from $MFT into a Resident subdirectory under --csv or --json"` // Включает сбор резидентных файлов
IncludeResidentData bool `name:"include-resident-data" default:"false" help:"Include resident data directly in CSV or JSON output"` // Включает вывод резидентных файлов в файл вывода
ResidentExt string `name:"resident-ext" help:"Comma-separated extensions to include for resident data, for example .txt,.ps1,.bat. Empty means all extensions"` // Расширения резидентных файлов
ResidentMaxBytes int `name:"resident-max-bytes" default:"1024" help:"Maximum resident data size to include in bytes. Max 1024000"` // Максимальный размер резидентных файлов
}
// Флаги настройки отображения $MFT
type MFTDisplayOptions struct {
ShortNames bool `name:"short-names" help:"Include DOS 8.3 short names in $MFT output"` // Сокращённые имена файлов DOS
FileList bool `name:"file-list" help:"Generate condensed file listing output. Requires --csv"` // Сжатый листинг файлов
AllFNTimes bool `name:"all-fn-times" help:"Include all timestamps from the 0x30 FILE_NAME attribute, not only those that differ from 0x10"` // Включает все временные метки для 0x30 атрибутов
RecoverSlack bool `name:"recover-slack" help:"Recover slack space from FILE records when processing $MFT"` // Восстанавливает slack space из FILE
}
// Флаги для парсинга с $MFT
type MFTParseCmd struct {
Input string `arg name:"path" help:"Path to the $MFT file." type:"path"` // Путь к $MFT
CSVOutput
JSONOutput
BodyOutput
VSSOptions
TimeFormatOption
MFTDisplayOptions
ResidentDataOptions
}
// Флаги для просмотра информации из $MFT
type MFTShowCmd struct {
Input string `arg name:"path" help:"Path to the $MFT file" type:"path"` // Путь к $MFT
Entry string `arg name:"entry" help:"Entry or entry-sequence. Examples: 5, 624-5, 0x270-0x5"` // Идентификатор сущности
ListDir bool `name:"list-dir" help:"If the selected entry is a directory, list its contents"` // Листинг директори
TimeFormatOption
}
// Флаги для извлечения записей из $MFT
type MFTExportRecordCmd struct {
Input string `arg name:"path" help:"Path to the $MFT file" type:"path"` // Путь к $MFT
Offset string `name:"offset" help:"FILE record offset in decimal or hex, for example 5120 or 0x1400" required` // Оффсет
OutDir string `name:"out" short:"o" help:"Directory to write the exported FILE record into" type:"path" required` // Директория для экспорта
}
// Флаги для парсинга $J
type JournalParseCmd struct {
Input string `arg:"" name:"path" help:"Path to the $J file." type:"path"` // Путь к $J
CSVOutput
JSONOutput
VSSOptions
TimeFormatOption
MFTPath string `name:"mft" short:"m" help:"Path to $MFT used to resolve parent paths in $J output." type:"path"` // Путь к $MFT
}
// Флаги для парсинга $Boot
type BootParseCmd struct {
Input string `arg:"" name:"path" help:"Path to the $Boot file." type:"path"` // Путь к $Boot
CSVOutput
VSSOptions
}
// Флаги для парсинга $SDS
type SDSParseCmd struct {
Input string `arg:"" name:"path" help:"Path to the $SDS file." type:"path"` // Путь к $SDS
CSVOutput
VSSOptions
}
// Флаги для просмотра идентификаторов $SDS
type SDSShowCmd struct {
Input string `arg:"" name:"path" help:"Path to the $SDS file." type:"path"` // Путь к $SDS
SecurityID string `arg:"" name:"security-id" help:"Security ID in decimal or hex, for example 624 or 0x270."` // Идентификатор
}
// Флаги для парсинга $I30
type I30ParseCmd struct {
Input string `arg:"" name:"path" help:"Path to the $I30 file." type:"path"` // Путь к $I30
CSVOutput
TimeFormatOption
}
// Флаги для парсинга $LogFile
type LogFileParseCmd struct {
Input string `arg:"" name:"path" help:"Path to the $LogFile file." type:"path"` // Путь к $LogFile
}

211
internal/cmd/cmd.go Normal file
View File

@@ -0,0 +1,211 @@
package cmd
import (
"errors"
"fmt"
"reflect"
"strings"
)
// Проверка пути на пустое значение
// Принимает на вход экземпляр bodyOutput и возвращает флаг, означающий непустой путь
func (o CSVOutput) Enabled() bool {
return strings.TrimSpace(o.CSVPath) != ""
}
// Проверка пути на пустое значение
// Принимает на вход экземпляр bodyOutput и возвращает флаг, означающий непустой путь
func (o JSONOutput) Enabled() bool {
return strings.TrimSpace(o.JSONPath) != ""
}
// Проверка пути на пустое значение.
// Принимает на вход экземпляр bodyOutput и возвращает флаг, означающий непустой путь
func (o BodyOutput) Enabled() bool {
return strings.TrimSpace(o.BodyPath) != ""
}
// Валидация флагов
// Принимает на вход указатель на экземпляр mftParseCmd и возвращает ошибку
func (c *MFTParseCmd) Validate() error {
if !c.CSVOutput.Enabled() && !c.JSONOutput.Enabled() && !c.BodyOutput.Enabled() {
return fmt.Errorf("mft parse requires at least one output: --csv, --json, or --body") // Проверка на наличие флага формата файла вывода
}
if c.BodyOutput.Enabled() && strings.TrimSpace(c.DriveLetter) == "" {
return fmt.Errorf("--drive-letter is required when using --body") // Проверка наличия буквы диска в Bodyfile
}
if c.FileList && !c.CSVOutput.Enabled() {
return fmt.Errorf("--file-list requires --csv") // Проверка CSV вывода для листинга файлов
}
if (c.DumpResidentFiles || c.IncludeResidentData) && !c.CSVOutput.Enabled() && !c.JSONOutput.Enabled() {
return fmt.Errorf("--dump-resident-files and --include-resident-data require --csv or --json") // Проверка CSV или JSON вывода для резидентных файлов
}
if c.ResidentMaxBytes <= 0 || c.ResidentMaxBytes > MaxResidentDataBytes {
return fmt.Errorf("--resident-max-bytes must be between 1 and %d", MaxResidentDataBytes) // Проверка максимального значения размера резидентных файлов
}
return nil
}
// Валидация флагов
// Принимает на вход указатель на экземпляр mftShowCmd и возвращает ошибку
func (c *MFTShowCmd) Validate() error {
return nil
}
// Валидация флагов
// Принимает на вход указатель на экземпляр mftExportRecordCmd и возвращает ошибку
func (c *MFTExportRecordCmd) Validate() error {
return nil
}
// Валидация флагов
// Принимает на вход указатель на экземпляр journalParseCmd и возвращает ошибку
func (c *JournalParseCmd) Validate() error {
if !c.CSVOutput.Enabled() && !c.JSONOutput.Enabled() {
return fmt.Errorf("j parse requires at least one output: --csv or --json") // Проверка вывода
}
return nil
}
// Валидация флагов
// Принимает на вход указатель на экземпляр bootParseCmd и возвращает ошибку
func (c *BootParseCmd) Validate() error {
return nil
}
// Валидация флагов
// Принимает на вход указатель на экземпляр sdsParseCmd и возвращает ошибку
func (c *SDSParseCmd) Validate() error {
if !c.CSVOutput.Enabled() {
return fmt.Errorf("sds parse requires --csv") // Проверка вывода в CSV
}
return nil
}
// Валидация флагов
// Принимает на вход указатель на экземпляр sdsShowCmd и возвращает ошибку
func (c *SDSShowCmd) Validate() error {
return nil
}
// Валидация флагов
// Принимает на вход указатель на экземпляр i30ParseCmd и возвращает ошибку
func (c *I30ParseCmd) Validate() error {
if !c.CSVOutput.Enabled() {
return fmt.Errorf("i30 parse requires --csv") // Проверка вывода в CSV
}
return nil
}
// Валидация флагов.
// Принимает на вход указатель на экземпляр logFileParseCmd и возвращает ошибку
func (c *LogFileParseCmd) Validate() error {
return nil
}
// Валидация команд.
// Принимает на вход объект и при наличии метода Validate производит валидацию
func ValidateLeaf(cmd any) error {
switch v := cmd.(type) {
case interface{ Validate() error }:
return v.Validate()
default:
return nil
}
}
// Валидация путей.
// Принимает на вход объект и при наличии метода Enabled производит валидацию
func ValidatePath(cmd any) error {
switch v := cmd.(type) {
case interface{ Enabled() error }:
return v.Enabled()
default:
return nil
}
}
// Проверяет, что у команды с output-блоками включён хотя бы один вывод.
func ValidateOutput(cmd any) error {
if cmd == nil {
return nil
}
v := reflect.ValueOf(cmd)
if v.Kind() == reflect.Pointer {
if v.IsNil() {
return nil
}
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return nil
}
var hasOutputBlocks bool
var hasEnabledOutput bool
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
fieldType := v.Type().Field(i)
switch fieldType.Type {
case reflect.TypeOf(CSVOutput{}):
hasOutputBlocks = true
if enabled, ok := callEnabled(field); ok && enabled {
hasEnabledOutput = true
}
case reflect.TypeOf(JSONOutput{}):
hasOutputBlocks = true
if enabled, ok := callEnabled(field); ok && enabled {
hasEnabledOutput = true
}
case reflect.TypeOf(BodyOutput{}):
hasOutputBlocks = true
if enabled, ok := callEnabled(field); ok && enabled {
hasEnabledOutput = true
}
}
}
if hasOutputBlocks && !hasEnabledOutput {
return errors.New("you must specify at least one output path: --csv, --json or --body")
}
return nil
}
// Вызов Enabled у поля
// Возвращает результат Enabled() и флаг успешности поиска метода
func callEnabled(v reflect.Value) (bool, bool) {
if !v.IsValid() {
return false, false
}
// Пробуем метод у самого значения
if method := v.MethodByName("Enabled"); method.IsValid() {
if method.Type().NumIn() == 0 &&
method.Type().NumOut() == 1 &&
method.Type().Out(0).Kind() == reflect.Bool {
out := method.Call(nil)
return out[0].Bool(), true
}
}
// Пробуем метод у указателя
if v.CanAddr() {
if method := v.Addr().MethodByName("Enabled"); method.IsValid() {
if method.Type().NumIn() == 0 &&
method.Type().NumOut() == 1 &&
method.Type().Out(0).Kind() == reflect.Bool {
out := method.Call(nil)
return out[0].Bool(), true
}
}
}
return false, false
}

51
internal/cmd/render.go Normal file
View File

@@ -0,0 +1,51 @@
package cmd
import (
"os"
"github.com/alecthomas/kong"
)
// Структура CLI
type CLI struct {
parser *kong.Kong
cmd CMD
}
// Создания экземпляра CLI
func NewCLI() *CLI {
c := &CLI{}
c.parser = kong.Must(
&c.cmd,
kong.Name("mftecmd"),
kong.Description("Utility for processing $MFT, $J, $LogFile, $Boot, $SDS, $I30"),
)
return c
}
// Метод запуска CLI
func (c *CLI) Run() *CMD {
// Парсинг флагов и аргументов
ctx, err := c.parser.Parse(os.Args[1:])
if err != nil {
c.parser.FatalIfErrorf(err)
}
// Валидируем команды
err = ValidateLeaf(c)
if err != nil {
c.parser.FatalIfErrorf(err)
}
// Валидируем путь вывода
err = ValidateOutput(c)
if err != nil {
c.parser.FatalIfErrorf(err)
}
_ = ctx
return &c.cmd
}

View File

View File

@@ -0,0 +1 @@
package mft

View File