appbinary-inspector 0.1.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.5
2
+ Name: appbinary-inspector
3
+ Version: 0.4.0
4
+ Summary: Open-source lazy-card character model plus static binary inspection tools.
5
+ Author: William Laurento
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3
11
+ Requires-Python: >=3.10
12
+ Provides-Extra: torch
13
+ Requires-Dist: torch>=2.1; extra == 'torch'
14
+ Description-Content-Type: text/markdown
15
+
16
+ # appbinary-inspector
17
+
18
+ Static inspection tools plus an open-source, disk-paged character-model architecture.
19
+
20
+ ## Features
21
+
22
+ - Detects PE, ELF, Mach-O, ZIP, 7z, and RAR headers
23
+ - Calculates SHA-256 and SHA-1 hashes
24
+ - Extracts readable strings and entropy
25
+ - Reads basic PE header metadata
26
+ - Extracts embedded ZIP archives and carves recognizable binary signatures
27
+ - Exports PE headers and raw binary sections for authorized analysis
28
+ - Builds a compact tokenizer for source code, logs, or extracted text
29
+ - Provides editable PyTorch classifier, autoencoder, and transformer templates
30
+ - Keeps CardStack model cards as separately stored 4-bit SSD files
31
+ - Loads only router-selected cards into a small LRU cache during generation
32
+ - Generates one next byte at a time for true character-level output
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install appbinary-inspector
38
+ ```
39
+
40
+ ## Use
41
+
42
+ ```bash
43
+ appbin inspect program.exe
44
+ appbin extract program.exe --output extracted
45
+ appbin binary-extract program.exe --output pe_parts
46
+ appbin tokenize source.py --vocab source-tokenizer.json
47
+ appbin model-template classifier
48
+ ```
49
+
50
+ ## CardStack: low-VRAM model experiments
51
+
52
+ CardStack is a small trainable byte-level language model. Its expert cards are
53
+ stored as separate 4-bit files. At inference, inactive cards remain on disk and
54
+ the cache retains only a limited number of recently selected cards.
55
+
56
+ ```python
57
+ from appbinary_inspector import CardStackConfig, CardStackLM, train_next_character
58
+
59
+ model = CardStackLM(CardStackConfig(width=128, layers=4, cards_per_layer=8, active_cards=2, cache_cards=4))
60
+ losses = train_next_character(model, open("training.txt", encoding="utf-8").read(), steps=500)
61
+ model.save("my-card-model") # writes a base file plus separate 4-bit card files
62
+
63
+ paged = CardStackLM.load("my-card-model")
64
+ print(paged.generate("Hello", new_characters=80))
65
+ ```
66
+
67
+ CLI version:
68
+
69
+ ```bash
70
+ appbin card-create --output my-card-model --width 128 --layers 4 --cards 8 --active-cards 2
71
+ appbin card-train my-card-model training.txt --steps 500
72
+ appbin card-generate my-card-model "Hello" --characters 80
73
+ ```
74
+
75
+ This reduces active model-card memory. It does not make a dense, unmodified
76
+ 70B model fit instantly on a low-VRAM GPU: disk bandwidth and CPU/GPU transfer
77
+ time become the tradeoff, and models need to be trained for sparse routing.
78
+
79
+ ## PyTorch models
80
+
81
+ Install optional PyTorch support:
82
+
83
+ ```bash
84
+ pip install "appbinary-inspector[torch]"
85
+ ```
86
+
87
+ ```python
88
+ from appbinary_inspector.torch_builder import create_model, preset
89
+
90
+ config = preset("classifier")
91
+ config["layers"][0]["out_features"] = 512
92
+ model = create_model(config)
93
+ ```
94
+
95
+ ## Safety
96
+
97
+ Targets are read as bytes only and never executed. The package does not defeat encryption, DRM, licensing controls, passwords, or code protections. It can analyze plaintext, export raw PE sections, and extract recognizable embedded content from files you are authorized to inspect.
@@ -0,0 +1,82 @@
1
+ # appbinary-inspector
2
+
3
+ Static inspection tools plus an open-source, disk-paged character-model architecture.
4
+
5
+ ## Features
6
+
7
+ - Detects PE, ELF, Mach-O, ZIP, 7z, and RAR headers
8
+ - Calculates SHA-256 and SHA-1 hashes
9
+ - Extracts readable strings and entropy
10
+ - Reads basic PE header metadata
11
+ - Extracts embedded ZIP archives and carves recognizable binary signatures
12
+ - Exports PE headers and raw binary sections for authorized analysis
13
+ - Builds a compact tokenizer for source code, logs, or extracted text
14
+ - Provides editable PyTorch classifier, autoencoder, and transformer templates
15
+ - Keeps CardStack model cards as separately stored 4-bit SSD files
16
+ - Loads only router-selected cards into a small LRU cache during generation
17
+ - Generates one next byte at a time for true character-level output
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pip install appbinary-inspector
23
+ ```
24
+
25
+ ## Use
26
+
27
+ ```bash
28
+ appbin inspect program.exe
29
+ appbin extract program.exe --output extracted
30
+ appbin binary-extract program.exe --output pe_parts
31
+ appbin tokenize source.py --vocab source-tokenizer.json
32
+ appbin model-template classifier
33
+ ```
34
+
35
+ ## CardStack: low-VRAM model experiments
36
+
37
+ CardStack is a small trainable byte-level language model. Its expert cards are
38
+ stored as separate 4-bit files. At inference, inactive cards remain on disk and
39
+ the cache retains only a limited number of recently selected cards.
40
+
41
+ ```python
42
+ from appbinary_inspector import CardStackConfig, CardStackLM, train_next_character
43
+
44
+ model = CardStackLM(CardStackConfig(width=128, layers=4, cards_per_layer=8, active_cards=2, cache_cards=4))
45
+ losses = train_next_character(model, open("training.txt", encoding="utf-8").read(), steps=500)
46
+ model.save("my-card-model") # writes a base file plus separate 4-bit card files
47
+
48
+ paged = CardStackLM.load("my-card-model")
49
+ print(paged.generate("Hello", new_characters=80))
50
+ ```
51
+
52
+ CLI version:
53
+
54
+ ```bash
55
+ appbin card-create --output my-card-model --width 128 --layers 4 --cards 8 --active-cards 2
56
+ appbin card-train my-card-model training.txt --steps 500
57
+ appbin card-generate my-card-model "Hello" --characters 80
58
+ ```
59
+
60
+ This reduces active model-card memory. It does not make a dense, unmodified
61
+ 70B model fit instantly on a low-VRAM GPU: disk bandwidth and CPU/GPU transfer
62
+ time become the tradeoff, and models need to be trained for sparse routing.
63
+
64
+ ## PyTorch models
65
+
66
+ Install optional PyTorch support:
67
+
68
+ ```bash
69
+ pip install "appbinary-inspector[torch]"
70
+ ```
71
+
72
+ ```python
73
+ from appbinary_inspector.torch_builder import create_model, preset
74
+
75
+ config = preset("classifier")
76
+ config["layers"][0]["out_features"] = 512
77
+ model = create_model(config)
78
+ ```
79
+
80
+ ## Safety
81
+
82
+ Targets are read as bytes only and never executed. The package does not defeat encryption, DRM, licensing controls, passwords, or code protections. It can analyze plaintext, export raw PE sections, and extract recognizable embedded content from files you are authorized to inspect.
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "appbinary-inspector"
7
+ version = "0.4.0"
8
+ description = "Open-source lazy-card character model plus static binary inspection tools."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "MIT"}
12
+ authors = [{name = "William Laurento"}]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: OS Independent",
17
+ ]
18
+ dependencies = []
19
+
20
+ [project.optional-dependencies]
21
+ torch = ["torch>=2.1"]
22
+
23
+ [project.scripts]
24
+ appbin = "appbinary_inspector.cli:main"
25
+
26
+ [tool.hatch.build.targets.wheel]
27
+ packages = ["src/appbinary_inspector"]
@@ -0,0 +1,9 @@
1
+ """Static binary inspection without executing the analyzed file."""
2
+
3
+ from .analyzer import inspect_file
4
+ from .extract import extract_binary_parts, extract_embedded
5
+ from .tokenizer import WordTokenizer
6
+ from .cardstack import CardStackConfig, CardStackLM, train_next_character
7
+
8
+ __all__ = ["inspect_file", "extract_embedded", "extract_binary_parts", "WordTokenizer", "CardStackConfig", "CardStackLM", "train_next_character"]
9
+ __version__ = "0.4.0"
@@ -0,0 +1,246 @@
1
+ """A compact, character-by-character language model with on-demand expert cards.
2
+
3
+ This is a research-sized model architecture, not a pre-trained replacement for
4
+ large commercial models. Each layer routes work to only ``active_cards`` expert
5
+ cards. Serialized cards live in separate files and are loaded into an LRU cache
6
+ only when the router selects them.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import math
12
+ from collections import OrderedDict
13
+ from dataclasses import asdict, dataclass
14
+ from pathlib import Path
15
+
16
+
17
+ def _torch():
18
+ try:
19
+ import torch
20
+ import torch.nn as nn
21
+ import torch.nn.functional as functional
22
+ return torch, nn, functional
23
+ except ImportError as error:
24
+ raise RuntimeError("Install PyTorch first: pip install appbinary-inspector[torch]") from error
25
+
26
+
27
+ @dataclass
28
+ class CardStackConfig:
29
+ width: int = 128
30
+ layers: int = 4
31
+ cards_per_layer: int = 8
32
+ active_cards: int = 2
33
+ context_size: int = 128
34
+ cache_cards: int = 4
35
+ card_bits: int = 4
36
+
37
+
38
+ def _pack_int4(values):
39
+ """Pack signed 4-bit values two per byte for SSD storage."""
40
+ torch, _, _ = _torch()
41
+ unsigned = (values.to(torch.int16) + 8).to(torch.uint8).flatten()
42
+ if unsigned.numel() % 2:
43
+ unsigned = torch.cat([unsigned, torch.zeros(1, dtype=torch.uint8)])
44
+ return unsigned[0::2] | (unsigned[1::2] << 4)
45
+
46
+
47
+ def _unpack_int4(packed, count: int):
48
+ torch, _, _ = _torch()
49
+ low = packed & 0x0F
50
+ high = packed >> 4
51
+ return (torch.stack((low, high), dim=1).flatten()[:count].to(torch.int16) - 8).to(torch.float32)
52
+
53
+
54
+ def quantize_card(state: dict) -> dict:
55
+ """Convert a card state dict to independently scaled 4-bit tensors."""
56
+ torch, _, _ = _torch()
57
+ tensors = {}
58
+ for name, tensor in state.items():
59
+ source = tensor.detach().cpu().float()
60
+ scale = max(float(source.abs().max()) / 7.0, 1e-8)
61
+ quantized = torch.round(source / scale).clamp(-8, 7).to("int8")
62
+ tensors[name] = {"shape": list(source.shape), "scale": scale, "count": source.numel(), "packed": _pack_int4(quantized)}
63
+ return {"format": "cardstack-int4-v1", "tensors": tensors}
64
+
65
+
66
+ def dequantize_card(payload: dict) -> dict:
67
+ """Restore a quantized card state dict just before the card is needed."""
68
+ if payload.get("format") != "cardstack-int4-v1":
69
+ return payload
70
+ torch, _, _ = _torch()
71
+ result = {}
72
+ for name, item in payload["tensors"].items():
73
+ values = _unpack_int4(item["packed"], item["count"]) * item["scale"]
74
+ result[name] = values.reshape(item["shape"])
75
+ return result
76
+
77
+
78
+ class CardStackLM:
79
+ """Byte-level LM. A generation step emits exactly one next byte/character."""
80
+
81
+ def __init__(self, config: CardStackConfig, card_directory: str | Path | None = None) -> None:
82
+ torch, nn, _ = _torch()
83
+ if not 1 <= config.active_cards <= config.cards_per_layer:
84
+ raise ValueError("active_cards must be between 1 and cards_per_layer")
85
+ self.torch, self.nn, self.config = torch, nn, config
86
+ self.embedding = nn.Embedding(256, config.width)
87
+ self.position = nn.Embedding(config.context_size, config.width)
88
+ self.routers = nn.ModuleList([nn.Linear(config.width, config.cards_per_layer) for _ in range(config.layers)])
89
+ self.norms = nn.ModuleList([nn.LayerNorm(config.width) for _ in range(config.layers)])
90
+ self.head = nn.Linear(config.width, 256)
91
+ self.card_directory = Path(card_directory) if card_directory else None
92
+ self.experts = nn.ModuleList() if not card_directory else None
93
+ if self.experts is not None:
94
+ for _ in range(config.layers):
95
+ self.experts.append(nn.ModuleList([self._new_card() for _ in range(config.cards_per_layer)]))
96
+ self.cache: OrderedDict[tuple[int, int], object] = OrderedDict()
97
+
98
+ def _new_card(self):
99
+ nn = self.nn
100
+ return nn.Sequential(nn.Linear(self.config.width, self.config.width * 2), nn.GELU(), nn.Linear(self.config.width * 2, self.config.width))
101
+
102
+ def _card(self, layer: int, index: int):
103
+ if self.experts is not None:
104
+ return self.experts[layer][index]
105
+ key = (layer, index)
106
+ if key in self.cache:
107
+ self.cache.move_to_end(key)
108
+ return self.cache[key]
109
+ if self.card_directory is None:
110
+ raise RuntimeError("No card storage configured")
111
+ card = self._new_card()
112
+ state = self.torch.load(self.card_directory / f"layer_{layer:02d}_card_{index:02d}.pt", map_location="cpu", weights_only=True)
113
+ state = dequantize_card(state)
114
+ card.load_state_dict(state)
115
+ card.eval()
116
+ self.cache[key] = card
117
+ while len(self.cache) > self.config.cache_cards:
118
+ self.cache.popitem(last=False)
119
+ return card
120
+
121
+ def prefetch(self, layer: int, card_indices: list[int]) -> None:
122
+ """Load likely next cards before the caller reaches that layer."""
123
+ if self.experts is None:
124
+ for index in card_indices:
125
+ self._card(layer, int(index))
126
+
127
+ def parameters(self):
128
+ modules = [self.embedding, self.position, self.routers, self.norms, self.head]
129
+ if self.experts is not None:
130
+ modules.append(self.experts)
131
+ for module in modules:
132
+ yield from module.parameters()
133
+
134
+ def train(self) -> None:
135
+ for module in (self.embedding, self.position, self.routers, self.norms, self.head):
136
+ module.train()
137
+ if self.experts is not None:
138
+ self.experts.train()
139
+
140
+ def eval(self) -> None:
141
+ for module in (self.embedding, self.position, self.routers, self.norms, self.head):
142
+ module.eval()
143
+ if self.experts is not None:
144
+ self.experts.eval()
145
+
146
+ def forward(self, token_ids):
147
+ torch = self.torch
148
+ _, sequence = token_ids.shape
149
+ if sequence > self.config.context_size:
150
+ token_ids = token_ids[:, -self.config.context_size:]
151
+ sequence = token_ids.shape[1]
152
+ positions = torch.arange(sequence, device=token_ids.device)
153
+ hidden = self.embedding(token_ids) + self.position(positions)[None, :, :]
154
+ for layer, (router, norm) in enumerate(zip(self.routers, self.norms)):
155
+ normalized = norm(hidden)
156
+ scores = router(normalized)
157
+ values, indices = scores.topk(self.config.active_cards, dim=-1)
158
+ # After layer 0 the current representation is a useful cheap hint for
159
+ # the next router, so prefetch its likely cards from SSD into the LRU.
160
+ if self.experts is None and layer + 1 < self.config.layers:
161
+ next_scores = self.routers[layer + 1](normalized)
162
+ next_indices = next_scores.topk(self.config.active_cards, dim=-1).indices.unique().tolist()
163
+ self.prefetch(layer + 1, next_indices)
164
+ weights = values.softmax(dim=-1)
165
+ routed = torch.zeros_like(hidden)
166
+ # Run only cards selected anywhere in this batch, then apply their gate.
167
+ for card_index in indices.unique().tolist():
168
+ card_output = self._card(layer, card_index)(normalized)
169
+ gate = (weights * (indices == card_index)).sum(dim=-1, keepdim=True)
170
+ routed = routed + card_output * gate
171
+ hidden = hidden + routed
172
+ return self.head(hidden)
173
+
174
+ def save(self, directory: str | Path, quantize_cards: bool = True) -> None:
175
+ if self.experts is None:
176
+ raise RuntimeError("A paged inference model cannot be saved as a new training bundle")
177
+ destination = Path(directory)
178
+ destination.mkdir(parents=True, exist_ok=True)
179
+ (destination / "config.json").write_text(json.dumps(asdict(self.config), indent=2), encoding="utf-8")
180
+ self.torch.save({"embedding": self.embedding.state_dict(), "position": self.position.state_dict(), "routers": self.routers.state_dict(), "norms": self.norms.state_dict(), "head": self.head.state_dict()}, destination / "base.pt")
181
+ for layer, cards in enumerate(self.experts):
182
+ for index, card in enumerate(cards):
183
+ payload = quantize_card(card.state_dict()) if quantize_cards else card.state_dict()
184
+ self.torch.save(payload, destination / f"layer_{layer:02d}_card_{index:02d}.pt")
185
+
186
+ @classmethod
187
+ def load(cls, directory: str | Path) -> "CardStackLM":
188
+ folder = Path(directory)
189
+ config = CardStackConfig(**json.loads((folder / "config.json").read_text(encoding="utf-8")))
190
+ model = cls(config, card_directory=folder)
191
+ state = model.torch.load(folder / "base.pt", map_location="cpu", weights_only=True)
192
+ for name in ("embedding", "position", "routers", "norms", "head"):
193
+ getattr(model, name).load_state_dict(state[name])
194
+ model.eval()
195
+ return model
196
+
197
+ @classmethod
198
+ def load_for_training(cls, directory: str | Path) -> "CardStackLM":
199
+ """Load every card into memory for training. Use ``load`` for low-memory inference."""
200
+ folder = Path(directory)
201
+ config = CardStackConfig(**json.loads((folder / "config.json").read_text(encoding="utf-8")))
202
+ model = cls(config)
203
+ state = model.torch.load(folder / "base.pt", map_location="cpu", weights_only=True)
204
+ for name in ("embedding", "position", "routers", "norms", "head"):
205
+ getattr(model, name).load_state_dict(state[name])
206
+ for layer, cards in enumerate(model.experts):
207
+ for index, card in enumerate(cards):
208
+ payload = model.torch.load(folder / f"layer_{layer:02d}_card_{index:02d}.pt", map_location="cpu", weights_only=True)
209
+ card.load_state_dict(dequantize_card(payload))
210
+ return model
211
+
212
+ def generate(self, prompt: str, new_characters: int = 80, temperature: float = 0.8) -> str:
213
+ torch = self.torch
214
+ if temperature <= 0:
215
+ raise ValueError("temperature must be positive")
216
+ values = list(prompt.encode("utf-8", "replace")) or [32]
217
+ self.eval()
218
+ with torch.no_grad():
219
+ for _ in range(new_characters):
220
+ input_ids = torch.tensor([values[-self.config.context_size:]], dtype=torch.long)
221
+ logits = self.forward(input_ids)[0, -1] / temperature
222
+ next_value = torch.multinomial(torch.softmax(logits, dim=-1), 1).item()
223
+ values.append(next_value)
224
+ return bytes(values).decode("utf-8", "replace")
225
+
226
+
227
+ def train_next_character(model: CardStackLM, text: str, steps: int = 500, learning_rate: float = 3e-4) -> list[float]:
228
+ """Train a small model from user-supplied text. Returns sampled loss values."""
229
+ torch, _, functional = _torch()
230
+ data = torch.tensor(list(text.encode("utf-8", "replace")), dtype=torch.long)
231
+ if len(data) <= model.config.context_size:
232
+ raise ValueError("Training text must exceed context_size bytes")
233
+ model.train()
234
+ optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate)
235
+ losses = []
236
+ for step in range(steps):
237
+ start = torch.randint(0, len(data) - model.config.context_size - 1, (1,)).item()
238
+ inputs = data[start:start + model.config.context_size].unsqueeze(0)
239
+ targets = data[start + 1:start + model.config.context_size + 1].unsqueeze(0)
240
+ optimizer.zero_grad()
241
+ loss = functional.cross_entropy(model.forward(inputs).reshape(-1, 256), targets.reshape(-1))
242
+ loss.backward()
243
+ optimizer.step()
244
+ if step % max(1, steps // 10) == 0 or step == steps - 1:
245
+ losses.append(round(float(loss.detach()), 4))
246
+ return losses
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ from pathlib import Path
6
+
7
+ from .analyzer import inspect_file
8
+ from .extract import extract_embedded
9
+ from .extract import extract_binary_parts
10
+ from .tokenizer import WordTokenizer
11
+ from .torch_builder import PRESETS
12
+ from .cardstack import CardStackConfig, CardStackLM, train_next_character
13
+
14
+ def main() -> None:
15
+ parser = argparse.ArgumentParser(description="Static application-binary inspector. Never executes targets.")
16
+ commands = parser.add_subparsers(dest="command", required=True)
17
+ inspect_cmd = commands.add_parser("inspect", help="inspect a binary")
18
+ inspect_cmd.add_argument("file")
19
+ inspect_cmd.add_argument("--strings", type=int, default=200)
20
+ extract_cmd = commands.add_parser("extract", help="extract recognizable embedded files")
21
+ extract_cmd.add_argument("file")
22
+ extract_cmd.add_argument("--output", required=True)
23
+ parts_cmd = commands.add_parser("binary-extract", help="export PE headers and raw sections")
24
+ parts_cmd.add_argument("file")
25
+ parts_cmd.add_argument("--output", required=True)
26
+ tokenize_cmd = commands.add_parser("tokenize", help="train a tokenizer and encode a text/source file")
27
+ tokenize_cmd.add_argument("file")
28
+ tokenize_cmd.add_argument("--vocab", default="tokenizer.json")
29
+ tokenize_cmd.add_argument("--max-vocab", type=int, default=20_000)
30
+ preset_cmd = commands.add_parser("model-template", help="print a PyTorch model configuration preset")
31
+ preset_cmd.add_argument("name", choices=sorted(PRESETS))
32
+ card_create = commands.add_parser("card-create", help="create a disk-paged character model")
33
+ card_create.add_argument("--output", required=True)
34
+ card_create.add_argument("--width", type=int, default=128)
35
+ card_create.add_argument("--layers", type=int, default=4)
36
+ card_create.add_argument("--cards", type=int, default=8)
37
+ card_create.add_argument("--active-cards", type=int, default=2)
38
+ card_create.add_argument("--cache-cards", type=int, default=4)
39
+ card_train = commands.add_parser("card-train", help="train a CardStack model from text")
40
+ card_train.add_argument("model")
41
+ card_train.add_argument("text_file")
42
+ card_train.add_argument("--steps", type=int, default=500)
43
+ card_generate = commands.add_parser("card-generate", help="generate one character at a time from disk-paged cards")
44
+ card_generate.add_argument("model")
45
+ card_generate.add_argument("prompt")
46
+ card_generate.add_argument("--characters", type=int, default=80)
47
+ args = parser.parse_args()
48
+ if args.command == "inspect":
49
+ result = inspect_file(args.file, args.strings)
50
+ elif args.command == "extract":
51
+ result = extract_embedded(args.file, args.output)
52
+ elif args.command == "binary-extract":
53
+ result = extract_binary_parts(args.file, args.output)
54
+ elif args.command == "tokenize":
55
+ text = Path(args.file).read_text(encoding="utf-8", errors="replace")
56
+ tokenizer = WordTokenizer().fit([text], max_vocab=args.max_vocab)
57
+ tokenizer.save(args.vocab)
58
+ result = {"vocabulary_path": args.vocab, "vocabulary_size": len(tokenizer.vocabulary), "tokens": tokenizer.tokenize(text), "ids": tokenizer.encode(text)}
59
+ elif args.command == "card-create":
60
+ config = CardStackConfig(width=args.width, layers=args.layers, cards_per_layer=args.cards, active_cards=args.active_cards, cache_cards=args.cache_cards)
61
+ CardStackLM(config).save(args.output)
62
+ result = {"model": args.output, "config": config.__dict__, "note": "Cards were saved in 4-bit files. Train before expecting useful output."}
63
+ elif args.command == "card-train":
64
+ model = CardStackLM.load_for_training(args.model)
65
+ losses = train_next_character(model, Path(args.text_file).read_text(encoding="utf-8", errors="replace"), steps=args.steps)
66
+ model.save(args.model)
67
+ result = {"model": args.model, "steps": args.steps, "loss_samples": losses}
68
+ elif args.command == "card-generate":
69
+ result = {"text": CardStackLM.load(args.model).generate(args.prompt, new_characters=args.characters)}
70
+ else:
71
+ result = PRESETS[args.name]
72
+ print(json.dumps(result, indent=2))
73
+
74
+ if __name__ == "__main__":
75
+ main()
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ import json
5
+ import struct
6
+ import zipfile
7
+ from pathlib import Path
8
+
9
+ from .formats import EMBEDDED_SIGNATURES
10
+
11
+
12
+ def extract_binary_parts(path: str | Path, output_dir: str | Path) -> dict:
13
+ """Write raw PE sections and a manifest. Reads bytes only; never executes code."""
14
+ source, destination = Path(path), Path(output_dir)
15
+ destination.mkdir(parents=True, exist_ok=True)
16
+ data = source.read_bytes()
17
+ manifest: dict = {"source": str(source), "size_bytes": len(data), "parts": []}
18
+ if not data.startswith(b"MZ") or len(data) < 0x40:
19
+ raise ValueError("binary-parts currently supports Windows PE files beginning with MZ")
20
+ pe_offset = struct.unpack_from("<I", data, 0x3C)[0]
21
+ if data[pe_offset:pe_offset + 4] != b"PE\0\0":
22
+ raise ValueError("invalid PE header")
23
+ _, section_count, _, _, _, optional_size, _ = struct.unpack_from("<HHIIIHH", data, pe_offset + 4)
24
+ header_end = pe_offset + 24 + optional_size + section_count * 40
25
+ header_path = destination / "pe_headers.bin"
26
+ header_path.write_bytes(data[:min(header_end, len(data))])
27
+ manifest["parts"].append({"name": "headers", "offset": 0, "size": header_path.stat().st_size, "path": str(header_path)})
28
+ table_offset = pe_offset + 24 + optional_size
29
+ for index in range(section_count):
30
+ entry = table_offset + index * 40
31
+ name = data[entry:entry + 8].split(b"\0", 1)[0].decode("ascii", "replace") or f"section_{index}"
32
+ raw_size, raw_offset = struct.unpack_from("<II", data, entry + 16)
33
+ content = data[raw_offset:min(raw_offset + raw_size, len(data))] if raw_offset < len(data) else b""
34
+ safe_name = "".join(char if char.isalnum() or char in "._-" else "_" for char in name)
35
+ section_path = destination / f"{index:02d}_{safe_name}.bin"
36
+ section_path.write_bytes(content)
37
+ manifest["parts"].append({"name": name, "offset": raw_offset, "size": len(content), "path": str(section_path)})
38
+ (destination / "manifest.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")
39
+ return manifest
40
+
41
+ def extract_embedded(path: str | Path, output_dir: str | Path, min_offset: int = 1) -> list[dict]:
42
+ """Carve recognizable embedded files without executing or decrypting content.
43
+
44
+ ZIP data is extracted only when it starts at a detected signature and passes
45
+ ZIP validation. Other formats are preserved from their signature to EOF for
46
+ analyst review, avoiding guesses about encrypted or packed payload bounds.
47
+ """
48
+ source = Path(path)
49
+ destination = Path(output_dir)
50
+ destination.mkdir(parents=True, exist_ok=True)
51
+ data = source.read_bytes()
52
+ found: list[dict] = []
53
+ seen: set[tuple[int, str]] = set()
54
+ for signature, (kind, extension) in EMBEDDED_SIGNATURES.items():
55
+ start = 0
56
+ while True:
57
+ index = data.find(signature, start)
58
+ if index < 0:
59
+ break
60
+ start = index + 1
61
+ if index < min_offset or (index, kind) in seen:
62
+ continue
63
+ seen.add((index, kind))
64
+ stem = f"{source.stem}_offset_{index:08x}"
65
+ if kind == "zip":
66
+ try:
67
+ with zipfile.ZipFile(io.BytesIO(data[index:])) as archive:
68
+ member_dir = destination / stem
69
+ member_dir.mkdir(exist_ok=True)
70
+ archive.extractall(member_dir)
71
+ found.append({"offset": index, "type": kind, "path": str(member_dir), "members": archive.namelist()})
72
+ except (zipfile.BadZipFile, OSError):
73
+ pass
74
+ else:
75
+ carved = destination / f"{stem}{extension}"
76
+ carved.write_bytes(data[index:])
77
+ found.append({"offset": index, "type": kind, "path": str(carved), "note": "carved from signature through EOF"})
78
+ return found
@@ -0,0 +1,48 @@
1
+ """Small, dependency-free tokenizer for source, logs, and extracted text."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import re
6
+ from collections import Counter
7
+ from pathlib import Path
8
+
9
+ TOKEN_PATTERN = re.compile(r"[A-Za-z_][A-Za-z_0-9]*|\d+(?:\.\d+)?|[^\s]", re.UNICODE)
10
+ SPECIAL_TOKENS = ("<pad>", "<unk>", "<bos>", "<eos>")
11
+
12
+
13
+ class WordTokenizer:
14
+ """Train a compact word/code tokenizer without external model downloads."""
15
+
16
+ def __init__(self, vocabulary: dict[str, int] | None = None) -> None:
17
+ self.vocabulary = vocabulary or {token: index for index, token in enumerate(SPECIAL_TOKENS)}
18
+ self.inverse_vocabulary = {index: token for token, index in self.vocabulary.items()}
19
+
20
+ @staticmethod
21
+ def tokenize(text: str) -> list[str]:
22
+ return TOKEN_PATTERN.findall(text)
23
+
24
+ def fit(self, texts: list[str], max_vocab: int = 20_000, min_frequency: int = 1) -> "WordTokenizer":
25
+ if max_vocab < len(SPECIAL_TOKENS):
26
+ raise ValueError("max_vocab must allow the special tokens")
27
+ counts = Counter(token for text in texts for token in self.tokenize(text))
28
+ self.vocabulary = {token: index for index, token in enumerate(SPECIAL_TOKENS)}
29
+ for token, count in counts.most_common(max_vocab - len(SPECIAL_TOKENS)):
30
+ if count < min_frequency:
31
+ continue
32
+ self.vocabulary[token] = len(self.vocabulary)
33
+ self.inverse_vocabulary = {index: token for token, index in self.vocabulary.items()}
34
+ return self
35
+
36
+ def encode(self, text: str, add_boundaries: bool = False) -> list[int]:
37
+ ids = [self.vocabulary.get(token, self.vocabulary["<unk>"]) for token in self.tokenize(text)]
38
+ return ([self.vocabulary["<bos>"]] + ids + [self.vocabulary["<eos>"]]) if add_boundaries else ids
39
+
40
+ def decode(self, ids: list[int]) -> list[str]:
41
+ return [self.inverse_vocabulary.get(index, "<unk>") for index in ids]
42
+
43
+ def save(self, path: str | Path) -> None:
44
+ Path(path).write_text(json.dumps({"vocabulary": self.vocabulary}, indent=2), encoding="utf-8")
45
+
46
+ @classmethod
47
+ def load(cls, path: str | Path) -> "WordTokenizer":
48
+ return cls(json.loads(Path(path).read_text(encoding="utf-8"))["vocabulary"])
@@ -0,0 +1,70 @@
1
+ """Config-first PyTorch model construction with no hidden training framework."""
2
+ from __future__ import annotations
3
+
4
+ from copy import deepcopy
5
+
6
+ PRESETS = {
7
+ "classifier": {"input_features": 128, "layers": [{"type": "linear", "out_features": 256}, {"type": "relu"}, {"type": "dropout", "p": 0.2}, {"type": "linear", "out_features": 10}]},
8
+ "autoencoder": {"input_features": 128, "layers": [{"type": "linear", "out_features": 64}, {"type": "relu"}, {"type": "linear", "out_features": 16}, {"type": "relu"}, {"type": "linear", "out_features": 64}, {"type": "relu"}, {"type": "linear", "out_features": 128}]},
9
+ "text_transformer": {"vocab_size": 20_000, "embedding_dim": 128, "sequence_length": 128, "transformer_layers": 2, "heads": 4, "classes": 2},
10
+ }
11
+
12
+
13
+ def preset(name: str) -> dict:
14
+ if name not in PRESETS:
15
+ raise ValueError(f"Unknown preset: {name}. Choose from {', '.join(PRESETS)}")
16
+ return deepcopy(PRESETS[name])
17
+
18
+
19
+ def create_model(config: dict):
20
+ """Build an nn.Module from a transparent config. Requires ``pip install appbinary-inspector[torch]``."""
21
+ try:
22
+ import torch.nn as nn
23
+ except ImportError as error:
24
+ raise RuntimeError("PyTorch is optional. Install it with: pip install appbinary-inspector[torch]") from error
25
+
26
+ if config.get("kind") == "transformer" or "vocab_size" in config:
27
+ required = ("vocab_size", "embedding_dim", "sequence_length", "transformer_layers", "heads", "classes")
28
+ missing = [key for key in required if key not in config]
29
+ if missing:
30
+ raise ValueError(f"Missing transformer settings: {', '.join(missing)}")
31
+ d_model = config["embedding_dim"]
32
+ encoder = nn.TransformerEncoder(
33
+ nn.TransformerEncoderLayer(d_model=d_model, nhead=config["heads"], batch_first=True),
34
+ num_layers=config["transformer_layers"],
35
+ )
36
+
37
+ class TokenClassifier(nn.Module):
38
+ def __init__(self) -> None:
39
+ super().__init__()
40
+ self.embedding = nn.Embedding(config["vocab_size"], d_model)
41
+ self.encoder = encoder
42
+ self.head = nn.Linear(d_model, config["classes"])
43
+
44
+ def forward(self, token_ids):
45
+ encoded = self.encoder(self.embedding(token_ids))
46
+ return self.head(encoded.mean(dim=1))
47
+
48
+ return TokenClassifier()
49
+
50
+ features = config.get("input_features")
51
+ if not isinstance(features, int) or features < 1:
52
+ raise ValueError("input_features must be a positive integer")
53
+ layers = []
54
+ for spec in config.get("layers", []):
55
+ kind = spec.get("type", "").lower()
56
+ if kind == "linear":
57
+ out_features = spec.get("out_features")
58
+ if not isinstance(out_features, int) or out_features < 1:
59
+ raise ValueError("linear layers require positive out_features")
60
+ layers.append(nn.Linear(features, out_features))
61
+ features = out_features
62
+ elif kind in {"relu", "gelu", "tanh", "sigmoid", "flatten"}:
63
+ layers.append({"relu": nn.ReLU, "gelu": nn.GELU, "tanh": nn.Tanh, "sigmoid": nn.Sigmoid, "flatten": nn.Flatten}[kind]())
64
+ elif kind == "dropout":
65
+ layers.append(nn.Dropout(float(spec.get("p", 0.5))))
66
+ else:
67
+ raise ValueError(f"Unsupported layer type: {kind}")
68
+ if not layers:
69
+ raise ValueError("config requires at least one layer")
70
+ return nn.Sequential(*layers)
@@ -1,41 +0,0 @@
1
- Metadata-Version: 2.5
2
- Name: appbinary-inspector
3
- Version: 0.1.0
4
- Summary: Safe static inspection and embedded-file extraction for application binaries.
5
- Author: William Laurento
6
- License: MIT
7
- License-File: LICENSE
8
- Classifier: License :: OSI Approved :: MIT License
9
- Classifier: Operating System :: OS Independent
10
- Classifier: Programming Language :: Python :: 3
11
- Requires-Python: >=3.10
12
- Description-Content-Type: text/markdown
13
-
14
- # appbinary-inspector
15
-
16
- Static inspection of application binaries without executing them.
17
-
18
- ## Features
19
-
20
- - Detects PE, ELF, Mach-O, ZIP, 7z, and RAR headers
21
- - Calculates SHA-256 and SHA-1 hashes
22
- - Extracts readable strings and entropy
23
- - Reads basic PE header metadata
24
- - Extracts embedded ZIP archives and carves recognizable binary signatures
25
-
26
- ## Install
27
-
28
- ```bash
29
- pip install appbinary-inspector
30
- ```
31
-
32
- ## Use
33
-
34
- ```bash
35
- appbin inspect program.exe
36
- appbin extract program.exe --output extracted
37
- ```
38
-
39
- ## Safety
40
-
41
- Targets are read as bytes only and never executed. The package does not defeat encryption, DRM, licensing controls, passwords, or code protections. It can analyze plaintext and extract recognizable embedded content from files you are authorized to inspect.
@@ -1,28 +0,0 @@
1
- # appbinary-inspector
2
-
3
- Static inspection of application binaries without executing them.
4
-
5
- ## Features
6
-
7
- - Detects PE, ELF, Mach-O, ZIP, 7z, and RAR headers
8
- - Calculates SHA-256 and SHA-1 hashes
9
- - Extracts readable strings and entropy
10
- - Reads basic PE header metadata
11
- - Extracts embedded ZIP archives and carves recognizable binary signatures
12
-
13
- ## Install
14
-
15
- ```bash
16
- pip install appbinary-inspector
17
- ```
18
-
19
- ## Use
20
-
21
- ```bash
22
- appbin inspect program.exe
23
- appbin extract program.exe --output extracted
24
- ```
25
-
26
- ## Safety
27
-
28
- Targets are read as bytes only and never executed. The package does not defeat encryption, DRM, licensing controls, passwords, or code protections. It can analyze plaintext and extract recognizable embedded content from files you are authorized to inspect.
@@ -1,20 +0,0 @@
1
- [build-system]
2
- requires = ["hatchling>=1.25"]
3
- build-backend = "hatchling.build"
4
-
5
- [project]
6
- name = "appbinary-inspector"
7
- version = "0.1.0"
8
- description = "Safe static inspection and embedded-file extraction for application binaries."
9
- readme = "README.md"
10
- requires-python = ">=3.10"
11
- license = {text = "MIT"}
12
- authors = [{name = "William Laurento"}]
13
- classifiers = ["Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent"]
14
- dependencies = []
15
-
16
- [project.scripts]
17
- appbin = "appbinary_inspector.cli:main"
18
-
19
- [tool.hatch.build.targets.wheel]
20
- packages = ["src/appbinary_inspector"]
@@ -1,7 +0,0 @@
1
- """Static binary inspection without executing the analyzed file."""
2
-
3
- from .analyzer import inspect_file
4
- from .extract import extract_embedded
5
-
6
- __all__ = ["inspect_file", "extract_embedded"]
7
- __version__ = "0.1.0"
@@ -1,23 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- import json
5
-
6
- from .analyzer import inspect_file
7
- from .extract import extract_embedded
8
-
9
- def main() -> None:
10
- parser = argparse.ArgumentParser(description="Static application-binary inspector. Never executes targets.")
11
- commands = parser.add_subparsers(dest="command", required=True)
12
- inspect_cmd = commands.add_parser("inspect", help="inspect a binary")
13
- inspect_cmd.add_argument("file")
14
- inspect_cmd.add_argument("--strings", type=int, default=200)
15
- extract_cmd = commands.add_parser("extract", help="extract recognizable embedded files")
16
- extract_cmd.add_argument("file")
17
- extract_cmd.add_argument("--output", required=True)
18
- args = parser.parse_args()
19
- result = inspect_file(args.file, args.strings) if args.command == "inspect" else extract_embedded(args.file, args.output)
20
- print(json.dumps(result, indent=2))
21
-
22
- if __name__ == "__main__":
23
- main()
@@ -1,41 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import io
4
- import zipfile
5
- from pathlib import Path
6
-
7
- from .formats import EMBEDDED_SIGNATURES
8
-
9
- def extract_embedded(path: str | Path, output_dir: str | Path, min_offset: int = 1) -> list[dict]:
10
- """Carve recognizable embedded files without executing or decrypting content."""
11
- source = Path(path)
12
- destination = Path(output_dir)
13
- destination.mkdir(parents=True, exist_ok=True)
14
- data = source.read_bytes()
15
- found: list[dict] = []
16
- seen: set[tuple[int, str]] = set()
17
- for signature, (kind, extension) in EMBEDDED_SIGNATURES.items():
18
- start = 0
19
- while True:
20
- index = data.find(signature, start)
21
- if index < 0:
22
- break
23
- start = index + 1
24
- if index < min_offset or (index, kind) in seen:
25
- continue
26
- seen.add((index, kind))
27
- stem = f"{source.stem}_offset_{index:08x}"
28
- if kind == "zip":
29
- try:
30
- with zipfile.ZipFile(io.BytesIO(data[index:])) as archive:
31
- member_dir = destination / stem
32
- member_dir.mkdir(exist_ok=True)
33
- archive.extractall(member_dir)
34
- found.append({"offset": index, "type": kind, "path": str(member_dir), "members": archive.namelist()})
35
- except (zipfile.BadZipFile, OSError):
36
- pass
37
- else:
38
- carved = destination / f"{stem}{extension}"
39
- carved.write_bytes(data[index:])
40
- found.append({"offset": index, "type": kind, "path": str(carved), "note": "carved from signature through EOF"})
41
- return found