torikago 1.3.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.
torikago-1.3.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nanodesu! contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,320 @@
1
+ Metadata-Version: 2.4
2
+ Name: torikago
3
+ Version: 1.3.0
4
+ Summary: Static triage for an unknown executable: identify, unpack, extract indicators. Never executes the target.
5
+ License: MIT
6
+ Keywords: malware,triage,torikago,pe,static-analysis,yara,dfir
7
+ Classifier: Environment :: Console
8
+ Classifier: Intended Audience :: Information Technology
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: Security
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Provides-Extra: pyinstaller
15
+ Requires-Dist: nanodesu<2,>=1.2; extra == "pyinstaller"
16
+ Dynamic: license-file
17
+
18
+ # Torikago
19
+
20
+ Static triage for an unknown executable. It tells you what a file really is, whether it
21
+ is packed, what is inside it, which indicators it carries, and hands you a draft YARA
22
+ rule — **without ever running it**.
23
+
24
+ The name is 鳥籠 — a birdcage. The metaphor is the whole tool: you put something live and
25
+ dangerous in a cage so you can **look at it from a safe distance**, held but in view. Which is
26
+ exactly what this does to an unknown sample, and why the tool refuses to run one.
27
+
28
+ The category word is still `triage`, and it stays in the description, the keywords and the
29
+ documentation, because a codename does not do ambient discovery — someone searching for a malware
30
+ triage tool should still land here. The name does the distinctive work; the words do the findable
31
+ work.
32
+
33
+ *Formerly published as `triage-static`.* The old name was a hyphenated compromise: `triage` on
34
+ PyPI was already taken by an unrelated risk-modelling package, so the package could not simply be
35
+ called what it was. `torikago` was free, and states something the old name could not.
36
+
37
+ ## The safety model, stated up front
38
+
39
+ **It never executes the target.** Not once, not on any code path. There is no
40
+ `CreateProcess` call in this tool, and there is no flag that adds one.
41
+
42
+ That is not a limitation, it is the entire safety argument. Unpacking is a byte-level
43
+ reading problem: a sample cannot act on a machine it is never allowed to run on. This is
44
+ a stronger guarantee than any user-mode sandbox, because it does not depend on catching
45
+ the sample's behaviour — there is no behaviour to catch.
46
+
47
+ Three things this tool therefore does **not** claim:
48
+
49
+ 1. **It is not a sandbox.** A user-mode process cannot contain a kernel-level or
50
+ administrator-level adversary. If a sample must be *run* to be understood, do it in a
51
+ disposable VM with no network and no shared folders. `torikago` prints that instruction
52
+ and refuses to take that step itself.
53
+ 2. **It is not an antivirus.** Detection means fixing a definition of "malicious", and
54
+ any definition can be bypassed — the reference implementations get bypass tools written
55
+ against them within days. The durable division of labour is: *this tool unpacks and
56
+ reports; an engine with maintained signatures decides.*
57
+ 3. **It cannot defeat a runtime packer.** Themida, VMProtect and custom stubs only reveal
58
+ themselves by running. `torikago` names them, records the evidence, and stops.
59
+
60
+ ## What it does
61
+
62
+ | Stage | Detail |
63
+ |---|---|
64
+ | **Identify** | magic bytes first, because extensions lie. A `invoice.png` that is really a PE is reported as such. |
65
+ | **PE structure** | machine, section table, per-section entropy, writable/executable flags, data directories (a TLS callback runs before the entry point and is called out). |
66
+ | **Imports** | full import table with per-DLL function lists. Dynamic resolution, injection and anti-debug APIs are flagged, and their weight is stated honestly: on their own they are ordinary program behaviour. |
67
+ | **Packer** | UPX (section pair and marker scan), Themida, VMProtect, ASPack, MPRESS, PECompact and friends, plus generic evidence (all sections high-entropy, a near-empty import table). |
68
+ | **Language runtime** | Names what built it: Rust (rustc markers), Go (build ID, runtime symbols), C#/.NET (decided structurally — COM descriptor → CLI header → `BSJB` metadata root, not by scanning for a four-byte signature), AutoIt, Delphi, Nim, Electron/Node, frozen Python. A Rust or Go binary whose symbols are gone is flagged, because stripping is normal and also removes the analyst's best tool. |
69
+ | **Wrappers** | PyInstaller cookies are read properly (python version, library, PYZ presence) and routed to `nanodesu.py` for unpacking. NSIS, Inno Setup, InstallShield, AutoIt, Nuitka and others are named, not pretended. |
70
+ | **Indicators** | URLs, IPs, domains, mail addresses, registry `Run` keys, services, scheduled tasks, PowerShell/cmd lines, named pipes, Defender exclusions. Documentation hosts and RFC1918 ranges are filtered; a version string like `6.0.0.0` is deliberately kept, because a false positive you can see beats one hidden from you. |
71
+ | **Strings** | ASCII and UTF-16LE (wide strings matter: a .NET or wide-char sample hides there), plus base64 blobs with their decoded heads, flagged when they decode to a PE. |
72
+ | **Embedded images** | PE files inside the file, located by header, offered for carving. |
73
+ | **Unpack (in-process)** | `--unpack` calls [Nanodesu!](https://github.com/Nesarf/Nanodesu) as a module — no subprocess, no shell — to actually unpack a PyInstaller archive, then triages the executables it produced. Set `NANODESU_PATH` if it is not in a known location. |
74
+ | **Batch scan** | `--scan DIR` triages every file in a directory and reports only the ones that stand out, ranked. On a real 200-file Python distribution it reports **0**; on a file carrying the injection triad it reports that file first. |
75
+ | **Debug information** | Reads the PE debug directory, and a `.pdb` beside the binary when one shipped. The CodeView record names the `.pdb`'s **absolute path on the build machine** — project, source layout, build configuration — with no second file needed. A shipped `.pdb` yields type names, method names and source paths. The two are matched by GUID, and a match that cannot be established is reported as *unverified* rather than as a mismatch |
76
+ | **Verdict** | `--scan-av` asks ClamAV; `--feed misp\|stix\|both` writes an importable MISP event and/or a STIX 2.1 bundle. |
77
+ | **Report** | `report.json` (machine-readable), a human summary, and `rule.yar` labelled `UNREVIEWED`. |
78
+
79
+ ## Install
80
+
81
+ No third-party dependencies, Python 3.9+.
82
+
83
+ ```bash
84
+ pip install . # provides the `torikago` command
85
+ python torikago.py --help # or run it directly
86
+ ```
87
+
88
+ For PyInstaller targets, either install the extra — `pip install torikago[pyinstaller]` —
89
+ or put `nanodesu.py` somewhere `torikago` can find it:
90
+
91
+ 1. `NANODESU_PATH`, pointing at a file or a directory
92
+ 2. an installed `nanodesu` module
93
+ 3. `nanodesu.py` next to `torikago`, or one directory up
94
+
95
+ `torikago` itself declares **no dependencies**, deliberately: it is meant to run on a machine you
96
+ do not trust, so every dependency it does not have is one less thing a reader has to audit. The
97
+ PyInstaller half is an extra rather than a requirement for that reason.
98
+
99
+ If it is absent, the failure says so and names the fix rather than leaving you to work it out:
100
+
101
+ ```
102
+ Nanodesu! was not found, so this PyInstaller archive cannot be unpacked.
103
+ Install it with: pip install nanodesu (or set NANODESU_PATH to a nanodesu.py checkout)
104
+ ```
105
+
106
+ It calls Nanodesu! through its library API when that is available, and falls back to the CLI for
107
+ versions before 1.3. Either way there is no subprocess and no shell in the middle.
108
+
109
+ ## Usage
110
+
111
+ ```bash
112
+ python torikago.py suspicious.exe
113
+ python torikago.py suspicious.exe -o ./out # also writes report.json and rule.yar
114
+ python torikago.py suspicious.exe --json # machine-readable on stdout
115
+ python torikago.py suspicious.exe --unpack # actually unpack it, then triage the inside
116
+ python torikago.py --scan ./downloads # which of these files deserves my time?
117
+ ```
118
+
119
+ Example summary:
120
+
121
+ ```
122
+ identified : PE executable (DOS/PE) (pe)
123
+ pe : x64, 7 sections, entry RVA 0xdcf0
124
+ .text 210432 raw entropy 6.49 X-
125
+ .rsrc 61440 raw entropy 7.35 --
126
+ wrapper : PyInstaller (python 3.12, python312.dll)
127
+ packer : none
128
+ imports : 132 functions across 3 DLL(s)
129
+ attention : uses APIs worth noting, though common in ordinary programs: ...
130
+ next steps :
131
+ [safe (no execution)] unpack the PyInstaller archive
132
+ python nanodesu.py extract <file> -o <out> --pyc
133
+ executed : no (never)
134
+ ```
135
+
136
+ Every entry in `next steps` is marked either `safe (no execution)` or
137
+ `needs execution`; anything in the second category comes with the instruction to do it in
138
+ a VM, and this tool does not do it for you.
139
+
140
+ ## Handing the evidence to something that decides
141
+
142
+ The durable division of labour: this tool unpacks and reports; something with maintained
143
+ signatures makes the call. Two targets cover most of the world, and both are read-only —
144
+ a scanner *reading* a file is not the file running, which is what lets the "never execute"
145
+ guarantee survive integration.
146
+
147
+ ### ClamAV
148
+
149
+ ```bash
150
+ python torikago.py suspicious.exe --scan-av --unpack
151
+ ```
152
+
153
+ `--scan-av` runs `clamscan` over the sample and anything the unpack produced, and reports
154
+ signature verdicts. It finds ClamAV on `PATH`, in the usual install locations, or via
155
+ `CLAMSCAN_PATH`. **If ClamAV is not installed, it says so and moves on** — a missing
156
+ scanner never turns into a broken feature.
157
+
158
+ ### MISP and STIX
159
+
160
+ ```bash
161
+ python torikago.py suspicious.exe --feed both -o ./out
162
+ ```
163
+
164
+ | File | Format | Purpose |
165
+ |---|---|---|
166
+ | `event.xml` | MISP event XML | import into MISP; hashes are `to_ids`, network indicators land in *Network activity*, persistence strings in *Artifacts dropped* |
167
+ | `stix.json` | STIX 2.1 bundle | for a TIP or MISP's STIX importer |
168
+
169
+ The MISP event is written as `published=false` on purpose: whether your indicators become
170
+ shared intelligence is a decision about your own data, not one a triage tool should make
171
+ for you.
172
+
173
+ ## Signals are graded, and the grading is the point
174
+
175
+ An import is not a verdict. `IsDebuggerPresent` is how CPython implements `sys.gettrace`;
176
+ `GetProcAddress` is how every delay-load stub works; `VirtualAlloc` is used by any JIT. A
177
+ scanner that flags those produces a list nobody reads.
178
+
179
+ So attention is raised by:
180
+
181
+ * the **injection triad** — `VirtualAlloc` + `WriteProcessMemory` + `CreateRemoteThread`
182
+ together, which is what process injection actually looks like
183
+ * a **packer verdict** built from section names, marker scans, or a genuinely empty import
184
+ table — not from "few imports", and not for an API-set forwarder
185
+ (`api-ms-win-*`), which has almost no imports by design
186
+ * a **high-entropy executable section**
187
+ * a **name that contradicts the contents**
188
+
189
+ Weak signals are recorded (`noted_imports`) without raising attention.
190
+
191
+ This is a calibration, not a heuristic: on a real 200-file Python distribution the scan
192
+ reports **zero** interesting files, and the same build of the tool still flags a synthetic
193
+ trojan carrying the injection triad and a misnamed extension. Both directions are tested.
194
+
195
+ ## Testing
196
+
197
+ ```bash
198
+ python -m unittest discover -s test -v
199
+ ```
200
+
201
+ 69 tests, no samples required: every fixture is a small synthetic file, including a
202
+ hand-assembled PE. A suite that needs real malware is a suite that stops being run.
203
+
204
+ The PE parser is additionally validated against real binaries, which is the baseline that
205
+ says the fixtures are not just self-consistent: `kernel32.dll` → 104 DLLs / 1274
206
+ functions, `advapi32.dll` → 35 / 656, `shell32.dll` → 79 / 1087.
207
+
208
+ ## Verified on
209
+
210
+ * A PyInstaller onefile build (windows x64) — identified, wrapper read correctly,
211
+ unpack routed to `nanodesu.py`.
212
+ * `7zr.exe`, `winmm.dll`, `kernel32.dll`, `advapi32.dll`, `shell32.dll` — structure and
213
+ import tables parsed, `packer: none` for unpacked binaries (no false packer verdicts).
214
+ * A `.png`-named PE — the naming contradiction is reported.
215
+
216
+ ## Where this sits, and where it does not
217
+
218
+ Several curated "top open-source security tools" lists were reviewed while building this
219
+ (secrss, eet-china, Tencent Cloud, Pa55w0rd/Enterprise_-Security_tools, and others). Across
220
+ all of them, **one tool does the same job and it works the opposite way**: Cuckoo Sandbox
221
+ ("constructs an isolated environment to *run* the malware and generates a behaviour log").
222
+ Everything else in the malware-adjacent categories is host-side or network-side detection —
223
+ Wazuh, OSSEC, whids, yulong-hids, Maltrail, Falco.
224
+
225
+ That is not a gap in those lists, it is the shape of the field:
226
+
227
+ ```
228
+ unknown file
229
+ -> [static] triage + Nanodesu! unpack <- never executes; this tool
230
+ -> [dynamic] Cuckoo / CAPEv2 detonation <- executes, in isolation
231
+ -> [verdict] ClamAV / YARA / reputation
232
+ -> [intel] MISP / Maltrail
233
+ ```
234
+
235
+ This tool is the stage *before* detonation: see what is inside safely, then decide whether
236
+ it is worth a sandbox slot. It deliberately does **not** become a sandbox, because that
237
+ requires running the sample, which is the one thing it will not do.
238
+
239
+ One useful pointer found in those lists: Microsoft's **RIFT**, for analysing **Rust**
240
+ malware. That is why language identification was added here — it is cheap, static, and Rust
241
+ binaries are harder to read on purpose.
242
+
243
+ ## Staging a flagged file for a VM
244
+
245
+ ```bash
246
+ python torikago.py suspicious.exe --quarantine ./shuttle
247
+ python torikago.py --quarantine-list ./shuttle
248
+ ```
249
+
250
+ A flagged file is **copied** into the shuttle directory with its verdict and the evidence
251
+ that produced it, and the output states the part that stays manual. Nothing is executed and
252
+ nothing is moved: the original stays where you can see it.
253
+
254
+ ```
255
+ staged : ./shuttle/20261003T215443_8f47b88ba3bf_invoice.png
256
+ reason : attention: injection triad (VirtualAlloc + WriteProcessMemory + CreateRemoteThread)
257
+ Next step is manual, in an isolated environment:
258
+ 1. create a disposable VM with no network and no shared folders
259
+ 2. copy ONLY this file in, and treat the copy as hostile
260
+ 3. run it there and observe; do not run it on the host
261
+ 4. destroy the VM afterwards rather than reusing it
262
+ ```
263
+
264
+ The reasoning: signature-based detection only recognises what it has already seen, and a
265
+ one-shot destroyer may never be seen twice. Staging puts a record and a copy in the
266
+ operator's hand **before** anything runs — and detonation stays a human decision, because the
267
+ alternative is a tool that launches malware by itself.
268
+
269
+ Three files land in the shuttle, and the name carries the hash so two files cannot silently
270
+ collide:
271
+
272
+ | File | Purpose |
273
+ |---|---|
274
+ | `<timestamp>_<sha256[:12]>_<name>` | the copy |
275
+ | the same name + `.why.json` | verdict, evidence, IOCs, and `executed: false` |
276
+ | `quarantine.jsonl` | append-only record for auditing |
277
+
278
+ ### What stages, and what does not
279
+
280
+ Calibrated on a corpus rather than guessed, because one signal turned out to be nearly
281
+ universal: **258 of 260 real system and application binaries report at least one "suspicious
282
+ string"**, so a keyword hit cannot decide anything on its own.
283
+
284
+ | Condition | Stages |
285
+ |---|---|
286
+ | destructive finding at high or critical severity | always |
287
+ | a high-confidence reason (injection triad, embedded boot sector, packing, high-entropy executable, a name that contradicts its contents) | yes, and the reason is quoted |
288
+ | weak signals only | needs several (`--quarantine-min`, default 1 for the strong gate) |
289
+ | a recognised wrapper | yes: it is a self-extracting program |
290
+ | nothing flagged | no — `nothing flagged above the staging threshold (weak signals: N)` |
291
+
292
+ ## Hardened as an attack surface, not only used as a tool
293
+
294
+ A tool that opens untrusted files is itself an attack surface, so it was audited as one.
295
+
296
+ | Area | State |
297
+ |---|---|
298
+ | **Never executes the target** | no `CreateProcess` anywhere, and no flag that adds one |
299
+ | **Resource bounds** | a single analysis loads the whole file, so a size guard refuses anything above **768 MB** by default (peak memory runs about twice the file size — a 300 MB file measured 600 MB and 62 seconds). `--max-bytes` raises it, `--force` overrides it, both explicit. Inside a directory scan only the first **4 MB** of each file is read. |
300
+ | **Subprocess** | ClamAV is invoked as an argument list, never through a shell. It is the only subprocess in the tool. |
301
+ | **Integration output** | the MISP event and STIX bundle embed attacker-controlled strings and are emitted through the standard serialisers, not by string concatenation |
302
+ | **Paths** | output paths come from the tool's own control, never from a name inside a sample — the one place a sample's names become paths is the unpack step, delegated to Nanodesu, where a path traversal bug was found and fixed in 1.1.0 |
303
+ | **Supply chain** | no third-party dependencies, no network access, CI actions pinned to commit SHAs, read-only CI token |
304
+
305
+ `SECURITY.md` states all of this, including what the tool explicitly does **not** defend
306
+ against, and private vulnerability reporting is enabled on the repository.
307
+
308
+ ## What was deliberately left out
309
+
310
+ * **Unpacking runtime packers.** Needs execution.
311
+ * **Network lookups.** No VirusTotal, no hash reputation, no telemetry. The tool works
312
+ on an air-gapped machine by design.
313
+ * **A detection engine.** See the safety model above. The signature engine is ClamAV; the
314
+ sharing formats are MISP and STIX. This tool's job is to make the sample legible to them.
315
+ * **Auto-removal of anything.** Left to engines that maintain signatures and to the
316
+ operator who knows the machine.
317
+
318
+ ## License
319
+
320
+ MIT.
@@ -0,0 +1,303 @@
1
+ # Torikago
2
+
3
+ Static triage for an unknown executable. It tells you what a file really is, whether it
4
+ is packed, what is inside it, which indicators it carries, and hands you a draft YARA
5
+ rule — **without ever running it**.
6
+
7
+ The name is 鳥籠 — a birdcage. The metaphor is the whole tool: you put something live and
8
+ dangerous in a cage so you can **look at it from a safe distance**, held but in view. Which is
9
+ exactly what this does to an unknown sample, and why the tool refuses to run one.
10
+
11
+ The category word is still `triage`, and it stays in the description, the keywords and the
12
+ documentation, because a codename does not do ambient discovery — someone searching for a malware
13
+ triage tool should still land here. The name does the distinctive work; the words do the findable
14
+ work.
15
+
16
+ *Formerly published as `triage-static`.* The old name was a hyphenated compromise: `triage` on
17
+ PyPI was already taken by an unrelated risk-modelling package, so the package could not simply be
18
+ called what it was. `torikago` was free, and states something the old name could not.
19
+
20
+ ## The safety model, stated up front
21
+
22
+ **It never executes the target.** Not once, not on any code path. There is no
23
+ `CreateProcess` call in this tool, and there is no flag that adds one.
24
+
25
+ That is not a limitation, it is the entire safety argument. Unpacking is a byte-level
26
+ reading problem: a sample cannot act on a machine it is never allowed to run on. This is
27
+ a stronger guarantee than any user-mode sandbox, because it does not depend on catching
28
+ the sample's behaviour — there is no behaviour to catch.
29
+
30
+ Three things this tool therefore does **not** claim:
31
+
32
+ 1. **It is not a sandbox.** A user-mode process cannot contain a kernel-level or
33
+ administrator-level adversary. If a sample must be *run* to be understood, do it in a
34
+ disposable VM with no network and no shared folders. `torikago` prints that instruction
35
+ and refuses to take that step itself.
36
+ 2. **It is not an antivirus.** Detection means fixing a definition of "malicious", and
37
+ any definition can be bypassed — the reference implementations get bypass tools written
38
+ against them within days. The durable division of labour is: *this tool unpacks and
39
+ reports; an engine with maintained signatures decides.*
40
+ 3. **It cannot defeat a runtime packer.** Themida, VMProtect and custom stubs only reveal
41
+ themselves by running. `torikago` names them, records the evidence, and stops.
42
+
43
+ ## What it does
44
+
45
+ | Stage | Detail |
46
+ |---|---|
47
+ | **Identify** | magic bytes first, because extensions lie. A `invoice.png` that is really a PE is reported as such. |
48
+ | **PE structure** | machine, section table, per-section entropy, writable/executable flags, data directories (a TLS callback runs before the entry point and is called out). |
49
+ | **Imports** | full import table with per-DLL function lists. Dynamic resolution, injection and anti-debug APIs are flagged, and their weight is stated honestly: on their own they are ordinary program behaviour. |
50
+ | **Packer** | UPX (section pair and marker scan), Themida, VMProtect, ASPack, MPRESS, PECompact and friends, plus generic evidence (all sections high-entropy, a near-empty import table). |
51
+ | **Language runtime** | Names what built it: Rust (rustc markers), Go (build ID, runtime symbols), C#/.NET (decided structurally — COM descriptor → CLI header → `BSJB` metadata root, not by scanning for a four-byte signature), AutoIt, Delphi, Nim, Electron/Node, frozen Python. A Rust or Go binary whose symbols are gone is flagged, because stripping is normal and also removes the analyst's best tool. |
52
+ | **Wrappers** | PyInstaller cookies are read properly (python version, library, PYZ presence) and routed to `nanodesu.py` for unpacking. NSIS, Inno Setup, InstallShield, AutoIt, Nuitka and others are named, not pretended. |
53
+ | **Indicators** | URLs, IPs, domains, mail addresses, registry `Run` keys, services, scheduled tasks, PowerShell/cmd lines, named pipes, Defender exclusions. Documentation hosts and RFC1918 ranges are filtered; a version string like `6.0.0.0` is deliberately kept, because a false positive you can see beats one hidden from you. |
54
+ | **Strings** | ASCII and UTF-16LE (wide strings matter: a .NET or wide-char sample hides there), plus base64 blobs with their decoded heads, flagged when they decode to a PE. |
55
+ | **Embedded images** | PE files inside the file, located by header, offered for carving. |
56
+ | **Unpack (in-process)** | `--unpack` calls [Nanodesu!](https://github.com/Nesarf/Nanodesu) as a module — no subprocess, no shell — to actually unpack a PyInstaller archive, then triages the executables it produced. Set `NANODESU_PATH` if it is not in a known location. |
57
+ | **Batch scan** | `--scan DIR` triages every file in a directory and reports only the ones that stand out, ranked. On a real 200-file Python distribution it reports **0**; on a file carrying the injection triad it reports that file first. |
58
+ | **Debug information** | Reads the PE debug directory, and a `.pdb` beside the binary when one shipped. The CodeView record names the `.pdb`'s **absolute path on the build machine** — project, source layout, build configuration — with no second file needed. A shipped `.pdb` yields type names, method names and source paths. The two are matched by GUID, and a match that cannot be established is reported as *unverified* rather than as a mismatch |
59
+ | **Verdict** | `--scan-av` asks ClamAV; `--feed misp\|stix\|both` writes an importable MISP event and/or a STIX 2.1 bundle. |
60
+ | **Report** | `report.json` (machine-readable), a human summary, and `rule.yar` labelled `UNREVIEWED`. |
61
+
62
+ ## Install
63
+
64
+ No third-party dependencies, Python 3.9+.
65
+
66
+ ```bash
67
+ pip install . # provides the `torikago` command
68
+ python torikago.py --help # or run it directly
69
+ ```
70
+
71
+ For PyInstaller targets, either install the extra — `pip install torikago[pyinstaller]` —
72
+ or put `nanodesu.py` somewhere `torikago` can find it:
73
+
74
+ 1. `NANODESU_PATH`, pointing at a file or a directory
75
+ 2. an installed `nanodesu` module
76
+ 3. `nanodesu.py` next to `torikago`, or one directory up
77
+
78
+ `torikago` itself declares **no dependencies**, deliberately: it is meant to run on a machine you
79
+ do not trust, so every dependency it does not have is one less thing a reader has to audit. The
80
+ PyInstaller half is an extra rather than a requirement for that reason.
81
+
82
+ If it is absent, the failure says so and names the fix rather than leaving you to work it out:
83
+
84
+ ```
85
+ Nanodesu! was not found, so this PyInstaller archive cannot be unpacked.
86
+ Install it with: pip install nanodesu (or set NANODESU_PATH to a nanodesu.py checkout)
87
+ ```
88
+
89
+ It calls Nanodesu! through its library API when that is available, and falls back to the CLI for
90
+ versions before 1.3. Either way there is no subprocess and no shell in the middle.
91
+
92
+ ## Usage
93
+
94
+ ```bash
95
+ python torikago.py suspicious.exe
96
+ python torikago.py suspicious.exe -o ./out # also writes report.json and rule.yar
97
+ python torikago.py suspicious.exe --json # machine-readable on stdout
98
+ python torikago.py suspicious.exe --unpack # actually unpack it, then triage the inside
99
+ python torikago.py --scan ./downloads # which of these files deserves my time?
100
+ ```
101
+
102
+ Example summary:
103
+
104
+ ```
105
+ identified : PE executable (DOS/PE) (pe)
106
+ pe : x64, 7 sections, entry RVA 0xdcf0
107
+ .text 210432 raw entropy 6.49 X-
108
+ .rsrc 61440 raw entropy 7.35 --
109
+ wrapper : PyInstaller (python 3.12, python312.dll)
110
+ packer : none
111
+ imports : 132 functions across 3 DLL(s)
112
+ attention : uses APIs worth noting, though common in ordinary programs: ...
113
+ next steps :
114
+ [safe (no execution)] unpack the PyInstaller archive
115
+ python nanodesu.py extract <file> -o <out> --pyc
116
+ executed : no (never)
117
+ ```
118
+
119
+ Every entry in `next steps` is marked either `safe (no execution)` or
120
+ `needs execution`; anything in the second category comes with the instruction to do it in
121
+ a VM, and this tool does not do it for you.
122
+
123
+ ## Handing the evidence to something that decides
124
+
125
+ The durable division of labour: this tool unpacks and reports; something with maintained
126
+ signatures makes the call. Two targets cover most of the world, and both are read-only —
127
+ a scanner *reading* a file is not the file running, which is what lets the "never execute"
128
+ guarantee survive integration.
129
+
130
+ ### ClamAV
131
+
132
+ ```bash
133
+ python torikago.py suspicious.exe --scan-av --unpack
134
+ ```
135
+
136
+ `--scan-av` runs `clamscan` over the sample and anything the unpack produced, and reports
137
+ signature verdicts. It finds ClamAV on `PATH`, in the usual install locations, or via
138
+ `CLAMSCAN_PATH`. **If ClamAV is not installed, it says so and moves on** — a missing
139
+ scanner never turns into a broken feature.
140
+
141
+ ### MISP and STIX
142
+
143
+ ```bash
144
+ python torikago.py suspicious.exe --feed both -o ./out
145
+ ```
146
+
147
+ | File | Format | Purpose |
148
+ |---|---|---|
149
+ | `event.xml` | MISP event XML | import into MISP; hashes are `to_ids`, network indicators land in *Network activity*, persistence strings in *Artifacts dropped* |
150
+ | `stix.json` | STIX 2.1 bundle | for a TIP or MISP's STIX importer |
151
+
152
+ The MISP event is written as `published=false` on purpose: whether your indicators become
153
+ shared intelligence is a decision about your own data, not one a triage tool should make
154
+ for you.
155
+
156
+ ## Signals are graded, and the grading is the point
157
+
158
+ An import is not a verdict. `IsDebuggerPresent` is how CPython implements `sys.gettrace`;
159
+ `GetProcAddress` is how every delay-load stub works; `VirtualAlloc` is used by any JIT. A
160
+ scanner that flags those produces a list nobody reads.
161
+
162
+ So attention is raised by:
163
+
164
+ * the **injection triad** — `VirtualAlloc` + `WriteProcessMemory` + `CreateRemoteThread`
165
+ together, which is what process injection actually looks like
166
+ * a **packer verdict** built from section names, marker scans, or a genuinely empty import
167
+ table — not from "few imports", and not for an API-set forwarder
168
+ (`api-ms-win-*`), which has almost no imports by design
169
+ * a **high-entropy executable section**
170
+ * a **name that contradicts the contents**
171
+
172
+ Weak signals are recorded (`noted_imports`) without raising attention.
173
+
174
+ This is a calibration, not a heuristic: on a real 200-file Python distribution the scan
175
+ reports **zero** interesting files, and the same build of the tool still flags a synthetic
176
+ trojan carrying the injection triad and a misnamed extension. Both directions are tested.
177
+
178
+ ## Testing
179
+
180
+ ```bash
181
+ python -m unittest discover -s test -v
182
+ ```
183
+
184
+ 69 tests, no samples required: every fixture is a small synthetic file, including a
185
+ hand-assembled PE. A suite that needs real malware is a suite that stops being run.
186
+
187
+ The PE parser is additionally validated against real binaries, which is the baseline that
188
+ says the fixtures are not just self-consistent: `kernel32.dll` → 104 DLLs / 1274
189
+ functions, `advapi32.dll` → 35 / 656, `shell32.dll` → 79 / 1087.
190
+
191
+ ## Verified on
192
+
193
+ * A PyInstaller onefile build (windows x64) — identified, wrapper read correctly,
194
+ unpack routed to `nanodesu.py`.
195
+ * `7zr.exe`, `winmm.dll`, `kernel32.dll`, `advapi32.dll`, `shell32.dll` — structure and
196
+ import tables parsed, `packer: none` for unpacked binaries (no false packer verdicts).
197
+ * A `.png`-named PE — the naming contradiction is reported.
198
+
199
+ ## Where this sits, and where it does not
200
+
201
+ Several curated "top open-source security tools" lists were reviewed while building this
202
+ (secrss, eet-china, Tencent Cloud, Pa55w0rd/Enterprise_-Security_tools, and others). Across
203
+ all of them, **one tool does the same job and it works the opposite way**: Cuckoo Sandbox
204
+ ("constructs an isolated environment to *run* the malware and generates a behaviour log").
205
+ Everything else in the malware-adjacent categories is host-side or network-side detection —
206
+ Wazuh, OSSEC, whids, yulong-hids, Maltrail, Falco.
207
+
208
+ That is not a gap in those lists, it is the shape of the field:
209
+
210
+ ```
211
+ unknown file
212
+ -> [static] triage + Nanodesu! unpack <- never executes; this tool
213
+ -> [dynamic] Cuckoo / CAPEv2 detonation <- executes, in isolation
214
+ -> [verdict] ClamAV / YARA / reputation
215
+ -> [intel] MISP / Maltrail
216
+ ```
217
+
218
+ This tool is the stage *before* detonation: see what is inside safely, then decide whether
219
+ it is worth a sandbox slot. It deliberately does **not** become a sandbox, because that
220
+ requires running the sample, which is the one thing it will not do.
221
+
222
+ One useful pointer found in those lists: Microsoft's **RIFT**, for analysing **Rust**
223
+ malware. That is why language identification was added here — it is cheap, static, and Rust
224
+ binaries are harder to read on purpose.
225
+
226
+ ## Staging a flagged file for a VM
227
+
228
+ ```bash
229
+ python torikago.py suspicious.exe --quarantine ./shuttle
230
+ python torikago.py --quarantine-list ./shuttle
231
+ ```
232
+
233
+ A flagged file is **copied** into the shuttle directory with its verdict and the evidence
234
+ that produced it, and the output states the part that stays manual. Nothing is executed and
235
+ nothing is moved: the original stays where you can see it.
236
+
237
+ ```
238
+ staged : ./shuttle/20261003T215443_8f47b88ba3bf_invoice.png
239
+ reason : attention: injection triad (VirtualAlloc + WriteProcessMemory + CreateRemoteThread)
240
+ Next step is manual, in an isolated environment:
241
+ 1. create a disposable VM with no network and no shared folders
242
+ 2. copy ONLY this file in, and treat the copy as hostile
243
+ 3. run it there and observe; do not run it on the host
244
+ 4. destroy the VM afterwards rather than reusing it
245
+ ```
246
+
247
+ The reasoning: signature-based detection only recognises what it has already seen, and a
248
+ one-shot destroyer may never be seen twice. Staging puts a record and a copy in the
249
+ operator's hand **before** anything runs — and detonation stays a human decision, because the
250
+ alternative is a tool that launches malware by itself.
251
+
252
+ Three files land in the shuttle, and the name carries the hash so two files cannot silently
253
+ collide:
254
+
255
+ | File | Purpose |
256
+ |---|---|
257
+ | `<timestamp>_<sha256[:12]>_<name>` | the copy |
258
+ | the same name + `.why.json` | verdict, evidence, IOCs, and `executed: false` |
259
+ | `quarantine.jsonl` | append-only record for auditing |
260
+
261
+ ### What stages, and what does not
262
+
263
+ Calibrated on a corpus rather than guessed, because one signal turned out to be nearly
264
+ universal: **258 of 260 real system and application binaries report at least one "suspicious
265
+ string"**, so a keyword hit cannot decide anything on its own.
266
+
267
+ | Condition | Stages |
268
+ |---|---|
269
+ | destructive finding at high or critical severity | always |
270
+ | a high-confidence reason (injection triad, embedded boot sector, packing, high-entropy executable, a name that contradicts its contents) | yes, and the reason is quoted |
271
+ | weak signals only | needs several (`--quarantine-min`, default 1 for the strong gate) |
272
+ | a recognised wrapper | yes: it is a self-extracting program |
273
+ | nothing flagged | no — `nothing flagged above the staging threshold (weak signals: N)` |
274
+
275
+ ## Hardened as an attack surface, not only used as a tool
276
+
277
+ A tool that opens untrusted files is itself an attack surface, so it was audited as one.
278
+
279
+ | Area | State |
280
+ |---|---|
281
+ | **Never executes the target** | no `CreateProcess` anywhere, and no flag that adds one |
282
+ | **Resource bounds** | a single analysis loads the whole file, so a size guard refuses anything above **768 MB** by default (peak memory runs about twice the file size — a 300 MB file measured 600 MB and 62 seconds). `--max-bytes` raises it, `--force` overrides it, both explicit. Inside a directory scan only the first **4 MB** of each file is read. |
283
+ | **Subprocess** | ClamAV is invoked as an argument list, never through a shell. It is the only subprocess in the tool. |
284
+ | **Integration output** | the MISP event and STIX bundle embed attacker-controlled strings and are emitted through the standard serialisers, not by string concatenation |
285
+ | **Paths** | output paths come from the tool's own control, never from a name inside a sample — the one place a sample's names become paths is the unpack step, delegated to Nanodesu, where a path traversal bug was found and fixed in 1.1.0 |
286
+ | **Supply chain** | no third-party dependencies, no network access, CI actions pinned to commit SHAs, read-only CI token |
287
+
288
+ `SECURITY.md` states all of this, including what the tool explicitly does **not** defend
289
+ against, and private vulnerability reporting is enabled on the repository.
290
+
291
+ ## What was deliberately left out
292
+
293
+ * **Unpacking runtime packers.** Needs execution.
294
+ * **Network lookups.** No VirusTotal, no hash reputation, no telemetry. The tool works
295
+ on an air-gapped machine by design.
296
+ * **A detection engine.** See the safety model above. The signature engine is ClamAV; the
297
+ sharing formats are MISP and STIX. This tool's job is to make the sample legible to them.
298
+ * **Auto-removal of anything.** Left to engines that maintain signatures and to the
299
+ operator who knows the machine.
300
+
301
+ ## License
302
+
303
+ MIT.