Documentation
¶
Overview ¶
Package gtbundle reads and writes the GopherTrunk Bundle — a single portable .gtb.tar.gz archive that packages one SDR capture-to-analysis "case": the raw IQ capture(s), a narrowband slice, the session logs, the SigLab signal-analysis result, the CryptoLab crypto-assessment result, and the hunt site/network mapping, all tied together by a human-and-machine readable MANIFEST.yaml.
One bundle carries a whole investigation so it can be shared with the community as a single file and flow seamlessly capture → SigLab → CryptoLab → back into the scanner config.
Disambiguation: the word "bundle" is overloaded in this repo. The hunt package's FormatBundle (internal/hunt/export.go) is a multi-section CSV that round-trips a discovered system into config.yaml — the "hunt CSV import bundle". That CSV is *stored inside* a GopherTrunk Bundle (as mapping/system.bundle.csv, role mapping-export); it is not this archive. This package is always spelled "GopherTrunk Bundle" / .gtb, and its Go identifier is gtbundle, never bundle.
On-disk layout, under a single top-level directory named after the bundle:
<name>/ MANIFEST.yaml index: schema_version, provenance, file inventory README.md auto-generated human summary notes.md free-form operator notes (optional) capture/ raw IQ (.cfile), narrowband slice (.wav), meta (.yaml) logs/ events.jsonl (all bus events), messages.log (human) mapping/ system.json (DiscoveredSystem), survey.jsonl, exports siglab/ result.yaml (siglab.Result), events.jsonl cryptolab/ frames.jsonl, assess.result.yaml (cryptolab.Result)
Serialization follows the repo conventions: YAML (snake_case) for the manifest and analysis summaries whose models carry yaml tags; JSON for hunt.DiscoveredSystem (which has only json tags); JSONL for streams; raw binary for IQ. Frequencies are integer Hz, timestamps UTC RFC3339.
Index ¶
- Constants
- Variables
- func MarshalManifest(m *Manifest) ([]byte, error)
- func MetadataToSigMF(m *siglab.Metadata) ([]byte, bool, error)
- func SanitizeName(s string) string
- type CaptureIntent
- type FileEntry
- type Generator
- type Manifest
- type Provenance
- type Reader
- func (r *Reader) Bytes(role Role) ([]byte, *FileEntry, error)
- func (r *Reader) CaptureIQ() ([]byte, *FileEntry, error)
- func (r *Reader) CaptureMeta() (*siglab.Metadata, error)
- func (r *Reader) CryptolabFramesRaw() ([]byte, error)
- func (r *Reader) CryptolabResultRaw() ([]byte, error)
- func (r *Reader) Extract(dstDir string) error
- func (r *Reader) ExtractRole(role Role, dstDir string) ([]string, error)
- func (r *Reader) FutureSchema() bool
- func (r *Reader) Manifest() *Manifest
- func (r *Reader) MappingSurvey() (*hunt.SignalSurvey, error)
- func (r *Reader) MappingSystem() (*hunt.DiscoveredSystem, error)
- func (r *Reader) Open(role Role) (io.ReadCloser, *FileEntry, error)
- func (r *Reader) SiglabResult() (*siglab.Result, error)
- func (r *Reader) Verify() []VerifyResult
- type Role
- type VerifyResult
- type Writer
- func (w *Writer) Add(role Role, leaf string, src io.Reader, producedBy string) (string, error)
- func (w *Writer) AddBytes(role Role, leaf string, body []byte, producedBy string) (string, error)
- func (w *Writer) AddJSON(role Role, leaf string, v any, producedBy string) (string, error)
- func (w *Writer) AddYAML(role Role, leaf string, v any, producedBy string) (string, error)
- func (w *Writer) Close() error
- func (w *Writer) Manifest() *Manifest
- func (w *Writer) SetNote(note string)
- type WriterOptions
Constants ¶
const ( // SchemaVersion is the on-disk manifest schema version. Readers accept any // bundle whose schema_version is <= this value; a higher version is read // best-effort with a warning rather than rejected (see reader.go). SchemaVersion = 1 // FormatID identifies the archive family; it is written into the manifest's // `format` field so a consumer can tell a GopherTrunk Bundle from any other // tar.gz at a glance. FormatID = "gophertrunk-bundle" // ManifestName / ReadmeName are the fixed filenames at the bundle root. ManifestName = "MANIFEST.yaml" ReadmeName = "README.md" // Ext is the conventional archive extension. Ext = ".gtb.tar.gz" )
Variables ¶
var ErrChecksum = errors.New("gtbundle: sha256 mismatch")
ErrChecksum is returned when a file's bytes do not match the sha256 recorded in the manifest.
var ErrFutureSchema = errors.New("gtbundle: manifest schema newer than supported")
ErrFutureSchema is returned (wrapped, non-fatal) by ParseManifest/Open when a bundle's schema_version is newer than this build supports. The bundle is still usable best-effort.
var ErrNotFound = errors.New("gtbundle: not found")
ErrNotFound is returned when a requested role/path is absent.
Functions ¶
func MarshalManifest ¶
MarshalManifest renders m as YAML with two-space indentation, matching the repo's siglab/cryptolab YAML export style.
func MetadataToSigMF ¶
MetadataToSigMF renders a siglab.Metadata as a SigMF .sigmf-meta document. It returns ok=false when the capture format has no SigMF datatype equivalent, so the caller can skip emitting a sidecar rather than write a malformed one.
func SanitizeName ¶
SanitizeName reduces a proposed bundle name to the safe token used as the single top-level directory inside the archive. It never returns "" (falls back to "bundle") and strips any leading dots/dashes so the name can't hide or look like a flag.
Types ¶
type CaptureIntent ¶
type CaptureIntent string
CaptureIntent classifies why a case was collected. It documents the operator's purpose and drives the default narrowband-slice length when a bundle is produced from a live capture.
const ( IntentCCMap CaptureIntent = "cc-map" // control-channel mapping / hunt IntentCrypto CaptureIntent = "crypto" // encryption assessment IntentSurvey CaptureIntent = "survey" // wideband classification survey IntentGeneral CaptureIntent = "general" // unspecified / general purpose )
func (CaptureIntent) Valid ¶
func (i CaptureIntent) Valid() bool
Valid reports whether i is a recognized intent.
type FileEntry ¶
type FileEntry struct {
Path string `yaml:"path"`
Role Role `yaml:"role"`
MediaType string `yaml:"media_type"`
SizeBytes int64 `yaml:"size_bytes"`
SHA256 string `yaml:"sha256"`
ProducedBy string `yaml:"produced_by,omitempty"`
Note string `yaml:"note,omitempty"`
}
FileEntry is one archived artifact. Path is bundle-relative (including the top-level dir) and always uses forward slashes, e.g. "mesa-p25/capture/cc.cfile". SHA256 is lowercase hex over the file bytes.
type Generator ¶
type Generator struct {
Tool string `yaml:"tool"`
Version string `yaml:"version"`
Commit string `yaml:"commit,omitempty"`
}
Generator records the tool that produced the bundle.
type Manifest ¶
type Manifest struct {
SchemaVersion int `yaml:"schema_version"`
Format string `yaml:"format"`
Name string `yaml:"name"`
CaptureIntent CaptureIntent `yaml:"capture_intent"`
CreatedAt time.Time `yaml:"created_at"`
Generator Generator `yaml:"generator"`
Provenance Provenance `yaml:"provenance"`
Title string `yaml:"title,omitempty"`
Notes string `yaml:"notes,omitempty"`
Files []FileEntry `yaml:"files"`
}
Manifest is the machine-and-human index stored at <name>/MANIFEST.yaml. It is YAML with snake_case keys. Unknown top-level keys and unknown file roles are tolerated on read so a newer bundle still opens in an older binary.
func ParseManifest ¶
ParseManifest decodes a MANIFEST.yaml. It validates the format id and that the schema version is not from the future beyond what this build supports; an unsupported-but-higher version is returned with a non-nil *Manifest and a non-fatal ErrFutureSchema so the caller can warn and proceed.
type Provenance ¶
type Provenance struct {
Source string `yaml:"source,omitempty"`
Operator string `yaml:"operator,omitempty"`
CenterFreqHz uint32 `yaml:"center_freq_hz,omitempty"`
SampleRateHz float64 `yaml:"sample_rate_hz,omitempty"`
Protocol string `yaml:"protocol,omitempty"`
Location string `yaml:"location,omitempty"`
CapturedAt time.Time `yaml:"captured_at,omitempty"`
DeviceDriver string `yaml:"device_driver,omitempty"`
DeviceSerial string `yaml:"device_serial,omitempty"`
}
Provenance is the capture-side context: where, when, and how the RF was collected. Every field is optional — a bundle packed from loose files may know little about the capture — but recording what is known makes a shared case reproducible. Frequencies are integer Hz; timestamps are UTC RFC3339.
type Reader ¶
type Reader struct {
// contains filtered or unexported fields
}
Reader provides random access to a bundle. Because tar is sequential, the whole archive is read into memory on open; bundles are small (a manifest plus a handful of analysis files and one raw IQ), and the large IQ member can be streamed straight to a working file with ExtractRole/CaptureIQ. A Reader is read-only and safe for concurrent reads after construction.
func (*Reader) Bytes ¶
Bytes returns the verified bytes of the first file with role, or ErrNotFound. The sha256 is checked against the manifest; a mismatch returns ErrChecksum.
func (*Reader) CaptureIQ ¶
CaptureIQ returns the verified raw-IQ bytes and its entry. For a large capture prefer ExtractRole(RoleCaptureIQ, dir) to stream it to a working file.
func (*Reader) CaptureMeta ¶
CaptureMeta decodes the capture metadata sidecar (siglab.Metadata, YAML).
func (*Reader) CryptolabFramesRaw ¶
CryptolabFramesRaw returns the verified bytes of the cryptolab frames stream (cryptolab/frames.jsonl). Returns ErrNotFound when absent.
func (*Reader) CryptolabResultRaw ¶
CryptolabResultRaw returns the verified bytes of the cryptolab result (cryptolab/assess.result.yaml). It is exposed as raw YAML rather than a typed accessor so this package needn't import internal/cryptolab; the caller (which already links cryptolab) decodes it. Returns ErrNotFound when absent.
func (*Reader) Extract ¶
Extract writes the whole bundle tree under dstDir, verifying each listed file as it goes. Untracked members are extracted too (best-effort). It re-validates every member path against traversal.
func (*Reader) ExtractRole ¶
ExtractRole writes just the files of one role under dstDir (flattened to their leaf names) and returns the written paths. Handy for handing one subtree onward — e.g. the cryptolab frames to a colleague.
func (*Reader) FutureSchema ¶
FutureSchema reports whether the bundle declares a schema newer than this build supports (it was still read best-effort).
func (*Reader) MappingSurvey ¶
func (r *Reader) MappingSurvey() (*hunt.SignalSurvey, error)
MappingSurvey reconstructs a SignalSurvey from the survey NDJSON stream: one DetectedSignal per line. The System pointer is left nil (it lives in mapping/system.json); callers that need it use MappingSystem.
func (*Reader) MappingSystem ¶
func (r *Reader) MappingSystem() (*hunt.DiscoveredSystem, error)
MappingSystem decodes the discovered system map (hunt.DiscoveredSystem, JSON).
func (*Reader) SiglabResult ¶
SiglabResult decodes the SigLab analysis result (siglab.Result, YAML).
func (*Reader) Verify ¶
func (r *Reader) Verify() []VerifyResult
Verify checks every manifest file's size+sha256 and flags any archive member not listed in the manifest as Untracked (non-fatal). It returns one result per manifest file plus one per untracked member.
type Role ¶
type Role string
Role names one artifact's meaning inside a bundle. Roles are a closed, append-only vocabulary: new roles may be added but existing ones are never renamed or repurposed, so an old reader still understands an old role in a newer bundle. A role a reader does not recognize is tolerated (see the forward-compat rules in reader.go) — it is preserved verbatim and bucketed under misc/ on a rewrite.
const ( RoleCaptureIQ Role = "capture-iq" // capture/<stem>.cfile — raw IQ, source of truth RoleCaptureSlice Role = "capture-slice" // capture/<stem>.slice.wav — narrowband DDC slice RoleCaptureMeta Role = "capture-meta" // capture/<stem>.meta.yaml — siglab.Metadata RoleCaptureSigMF Role = "capture-sigmf" // capture/<stem>.sigmf-meta — optional SigMF sidecar RoleLogEvents Role = "log-events" // logs/events.jsonl — all bus events (EventLog) RoleLogMessages Role = "log-messages" // logs/messages.log — human decoded-message log RoleMappingSystem Role = "mapping-system" // mapping/system.json — hunt.DiscoveredSystem RoleMappingSurvey Role = "mapping-survey" // mapping/survey.jsonl — hunt.DetectedSignal stream RoleMappingExport Role = "mapping-export" // mapping/system.bundle.csv | .trunk-recorder.json | .rr.md RoleSiglabResult Role = "siglab-result" // siglab/result.yaml — siglab.Result RoleSiglabEvents Role = "siglab-events" // siglab/events.jsonl — siglab events/PDUs RoleCryptolabFrames Role = "cryptolab-frames" // cryptolab/frames.jsonl — cryptocap records RoleCryptolabResult Role = "cryptolab-result" // cryptolab/assess.result.yaml — cryptolab.Result RoleNotes Role = "notes" // notes.md — free-form operator notes RoleReadme Role = "readme" // README.md — auto-generated summary )
type VerifyResult ¶
type VerifyResult struct {
Path string
Role Role
OK bool
Untracked bool // present in archive but not in the manifest
Err error
}
VerifyResult is one file's integrity outcome.
type Writer ¶
type Writer struct {
// contains filtered or unexported fields
}
Writer assembles a .gtb.tar.gz. Files are streamed in with Add/AddFile/AddYAML as they are produced; MANIFEST.yaml and README.md are written by Close, which finalizes the archive. A Writer is not safe for concurrent use.
func NewWriter ¶
func NewWriter(w io.Writer, opts WriterOptions) (*Writer, error)
NewWriter wraps w in gzip+tar and prepares an in-memory manifest. The caller must Close the Writer to flush the archive. w is typically an *os.File.
func (*Writer) Add ¶
Add stages src under the canonical path for role (topDir/roleDir/leaf), computing its sha256 and size and appending a manifest entry. leaf must be a single path component; producedBy records the tool/step that made the file. It returns the bundle-relative path.
func (*Writer) AddJSON ¶
AddJSON marshals v as indented JSON and stages it. Use for hunt.DiscoveredSystem (json-only tags) so snake_case is preserved.
func (*Writer) AddYAML ¶
AddYAML marshals v as two-space YAML and stages it. Use for models with yaml tags (siglab.Metadata, siglab.Result, cryptolab.Result).
func (*Writer) Close ¶
Close writes MANIFEST.yaml and README.md, emits every staged file in deterministic role order, and flushes tar+gzip. It is safe to call once; a second call is a no-op.
type WriterOptions ¶
type WriterOptions struct {
Name string
CaptureIntent CaptureIntent
Provenance Provenance
Title string
Notes string
// Now is an injectable clock so tests can produce deterministic archives.
// When nil, time.Now is used. Its value stamps CreatedAt and every tar
// header ModTime, making a repack byte-identical for a fixed input set.
Now func() time.Time
}
WriterOptions configure a new bundle. Name is required; everything else has a sensible default.