mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
feat(elf): inspect Linux kernel module metadata (#7010)
* feat(elf): inspect Linux kernel module metadata Adds inspect-kernel-module and inspect_kernel_module for ELF32/ELF64 ET_REL Linux kernel modules, read statically without executing them. Reports ordered .modinfo entries with duplicates preserved, __versions symbol CRCs, the presence of .gnu.linkonce.this_module and __ksymtab, .init.text and .exit.text sizes, and an appended module signature trailer with its PKCS#7 byte range and SHA-256. gzip, xz and zstd containers are reported unsupported with external decompression as the next probe. Malformed headers and exhausted entry budgets return no partial success. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016y53PdpyMAp9RSW65fatHe * fix(elf): bound kernel module ksymtab listing and tighten contracts Charge listed __ksymtab* sections against max_entries, omit section names longer than 256 bytes with ksymtab_name_exceeds_limit, and aggregate the non-file-backed limitation so repeated huge names cannot inflate Evidence. Use fixedTupleSchema for the signature pad bytes with projection parity tests, and assert the late-cancellation reason and retained observation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016y53PdpyMAp9RSW65fatHe * fix: resolve merge integration errors --------- Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
57 files changed
+3160
-14
No files matched your search
@@ -284,6 +284,16 @@ explicit path or endpoint and do not need it.
|
||||
body; other types, including `"288"` stackshots, return no crash result.
|
||||
Opened Mach-O and dSYM UUID joins remain unobserved. See the
|
||||
[Apple IPS guide](https://github.com/morluto/rea/blob/main/docs/apple-ips.md).
|
||||
- Linux kernel module metadata: `inspect_kernel_module` with the explicit local
|
||||
ELF32/ELF64 relocatable file. This built-in target-free inspection requires
|
||||
no Linux host or `modinfo`. Preserve `.modinfo` key order and duplicate keys,
|
||||
`__versions` CRC hex, section file ranges, and the appended signature
|
||||
trailer's PKCS#7 SHA-256. A missing section is observed absence when section
|
||||
names are known, not proof the module never carried that metadata.
|
||||
`struct module` is not decoded. The signature is not validated. gzip, xz, and
|
||||
zstd containers are unsupported; decompress into a separate file with an
|
||||
external codec utility, then inspect the decompressed `ET_REL` image. See the
|
||||
[kernel module guide](https://github.com/morluto/rea/blob/main/docs/kernel-modules.md).
|
||||
- CPython 3.7–3.13 `.pyc` header, nested marshal code objects and bytecode
|
||||
identity: `inspect_python_pyc` with the explicit local file. This built-in
|
||||
target-free inspection requires no Python installation. Preserve magic,
|
||||
|
||||
@@ -161,6 +161,25 @@ uppercase image UUIDs, and integer literals that exceed IEEE-754 range. Only
|
||||
no crash result. Opened Mach-O and dSYM UUID joins remain unobserved. See
|
||||
[Apple IPS crash reports](https://github.com/morluto/rea/blob/main/docs/apple-ips.md).
|
||||
|
||||
## Linux kernel modules
|
||||
|
||||
Use `inspect_kernel_module` for an explicit local ELF32/ELF64 relocatable
|
||||
object when the question concerns `.modinfo` license, vermagic, alias,
|
||||
depends, or parm strings, the `__versions` symbol CRCs, or an appended module
|
||||
signature trailer. No `open_binary`, Linux host, or `modinfo` program is
|
||||
required. The CLI equivalent is `inspect-kernel-module PATH`. Preserve
|
||||
artifact SHA-256, key order, duplicate keys, CRC hex, and section file ranges.
|
||||
A missing section is observed absence when section names are known, not proof
|
||||
the module never carried that metadata. `.gnu.linkonce.this_module` is reported
|
||||
as present or absent; `struct module` stays unknown because its layout changes
|
||||
with the kernel version. `__ksymtab` records are not decoded. The PKCS#7 range
|
||||
and its SHA-256 are observed; certificate, signer, and digest coverage remain
|
||||
unvalidated. An absent trailer is not proof the module is unsigned in another
|
||||
container. gzip, xz, and zstd whole-file containers are unsupported and name
|
||||
`external_decompression` as the next probe: use an external codec utility to
|
||||
decompress into a separate file, then call `inspect_kernel_module` on that file. See
|
||||
[Linux kernel modules](https://github.com/morluto/rea/blob/main/docs/kernel-modules.md).
|
||||
|
||||
## CPython pyc files
|
||||
|
||||
Use `inspect_python_pyc` for an explicit local CPython 3.7–3.13 `.pyc` when the
|
||||
|
||||
@@ -195,6 +195,7 @@ Additional tools and host support depend on the target:
|
||||
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](docs/pe-imports.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](docs/elf-notes.md) |
|
||||
| Apple IPS crashes | .ips metadata, threads, images, registers and documented crash classes | Built-in static JSON inspection; [Apple IPS](docs/apple-ips.md) |
|
||||
| Linux kernel modules | .modinfo keys, __versions CRCs, this_module presence, and signature trailer | Built-in static ELF inspection; [kernel modules](docs/kernel-modules.md) |
|
||||
| Erlang BEAM | IFF chunks, atoms, exports, imports, lambdas and the Code header | Built-in static BEAM inspection; [Erlang BEAM](docs/erlang-beam.md) |
|
||||
| WASM artifact | Validation, sections, exact import/export WAT forms and decoded WAT | Caller-supplied WABT 1.0.42; `inspect_wasm_artifact` |
|
||||
| EVM bytecode | Dispatch selectors, byte offsets, inferred arguments and mutability | Local raw/hex carrier on Linux x64; [offline EVM guide](docs/evm-bytecode.md) |
|
||||
|
||||
@@ -37,6 +37,7 @@ export default defineConfig({
|
||||
{ text: "PE TLS/load config", link: "/pe-load-config" },
|
||||
{ text: "ELF notes", link: "/elf-notes" },
|
||||
{ text: "Apple IPS crash reports", link: "/apple-ips" },
|
||||
{ text: "Linux kernel modules", link: "/kernel-modules" },
|
||||
{ text: "CPython pyc files", link: "/python-pyc" },
|
||||
{ text: "Erlang BEAM", link: "/erlang-beam" },
|
||||
{
|
||||
|
||||
+16
@@ -321,6 +321,22 @@ The workflow reports first-line metadata, process/exception/termination facts,
|
||||
threads, usedImages, lossless integer registers, and documented crash classes
|
||||
when `bug_type` is `"309"`. Other DiagnosticReports types return no crash
|
||||
result. See [Apple IPS crash reports](apple-ips.md).
|
||||
Inspect Linux kernel module metadata without executing the module or requiring
|
||||
`modinfo`:
|
||||
|
||||
```bash
|
||||
rea inspect-kernel-module /absolute/path/to/module.ko --json
|
||||
```
|
||||
|
||||
The workflow reports ordered `.modinfo` keys (duplicate `alias` and `parm`
|
||||
entries preserved), `__versions` symbol CRCs, presence of
|
||||
`.gnu.linkonce.this_module` and `__ksymtab` sections, observed `.init.text`
|
||||
and `.exit.text` sizes, and an appended signature trailer. `struct module` is
|
||||
not decoded and the signature is not validated. Other ELF types are not
|
||||
applicable. gzip, xz, and zstd containers are unsupported and name
|
||||
`external_decompression` as the next probe: decompress with an external
|
||||
utility, then inspect the decompressed file. See
|
||||
[Linux kernel modules](kernel-modules.md).
|
||||
|
||||
Inspect a CPython 3.7–3.13 `.pyc` header and nested marshal code objects
|
||||
without executing bytecode or installing Python:
|
||||
|
||||
+14
-13
@@ -47,19 +47,20 @@ For runtime instrumentation, see the [Frida guide](./frida-instrumentation.md).
|
||||
|
||||
## Choose a guide
|
||||
|
||||
| Task | Guides |
|
||||
| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Identify a desktop installation | [Runtime candidates, evidence and follow-up files](desktop-applications.md) |
|
||||
| Inspect native functions, references, or types | [Native binaries](native-investigation.md), [Go metadata](go-binaries.md), [Rust dependencies](rust-binaries.md), [CPython pyc files](python-pyc.md), [PE certificates](pe-certificates.md), [PE imports](pe-imports.md), [PE TLS/load config](pe-load-config.md), [ELF notes](elf-notes.md), [Apple IPS](apple-ips.md), [Erlang BEAM](erlang-beam.md), [offline ELF layout](binary-diagnostics.md), [recorded crashes](recorded-crashes.md) |
|
||||
| Select a native provider or import format | [Ghidra semantics](ghidra-provider.md), [Windows](windows-ghidra-p0.md), [DOS](ghidra-dos.md), [raw bytes](raw-binary.md), [GameCube DOL](ghidra-dol.md), [MIPS](ghidra-mips.md), [PSP](ghidra-psp.md), [NativeAOT](ghidra-nativeaot.md), [IDA](ida-provider.md), [Kuna](kuna-provider.md), [JEB](jeb-analysis.md) |
|
||||
| Analyze JavaScript or Electron artifacts | [Static analysis](javascript-artifact-reconstruction.md), [tracing and comparisons](javascript-application-workflows.md), [application graph](javascript-application-graph.md), [source recovery](javascript-recovery.md) |
|
||||
| Compare static JavaScript with runtime observations | [Runtime attribution](javascript-runtime-observation.md), [reconciliation](javascript-runtime-reconciliation.md), [Electron observation](electron-observation.md), [Electron fuses](electron-fuses.md) |
|
||||
| Inspect assemblies, packages, or firmware | [Godot PCK](godot-pck.md), [.NET](managed-code-analysis.md), [Python packages](python-packaged-applications.md), [Android](android-analysis.md), [APK signatures](apk-signature.md), [Apktool resources](apktool-resource-analysis.md), [ADB acquisition](adb-device-analysis.md), [Apple](apple-application-analysis.md), [Harmony](harmony-analysis.md), [firmware](firmware-analysis.md), [WASM](wasm-artifacts.md), [EVM](evm-bytecode.md) |
|
||||
| Observe or compare a browser scenario | [CDP observation](browser-observation.md), [scenarios](browser-scenario-contract.md), [runtime evidence](web-runtime.md), [historical captures](web-network-captures.md) |
|
||||
| Serve recorded behavior as an agent environment | [Agent environments](agent-environments.md) |
|
||||
| Trace captured website source | [Source locations](web-source-location.md), [module traces](website-module-trace.md), [script export](website-script-export.md) |
|
||||
| Capture and compare a command | [Process capture](process-capture.md) |
|
||||
| Use CLI/MCP and retain evidence | [CLI](cli.md), [MCP contracts](mcp-contracts.md), [agent prompts](mcp-prompts.md), [obligation ledgers](reconstruction-obligation-ledgers.md) |
|
||||
| Task | Guides |
|
||||
| --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Identify a desktop installation | [Runtime candidates, evidence and follow-up files](desktop-applications.md) |
|
||||
| Inspect native functions, references, or types | [Native binaries](native-investigation.md), [Go metadata](go-binaries.md), [Rust dependencies](rust-binaries.md), [CPython pyc files](python-pyc.md), [PE certificates](pe-certificates.md), [PE imports](pe-imports.md), [PE TLS/load config](pe-load-config.md), [ELF notes](elf-notes.md), [Linux kernel modules](kernel-modules.md), [Apple IPS](apple-ips.md), [Erlang BEAM](erlang-beam.md), [offline ELF layout](binary-diagnostics.md), [recorded crashes](recorded-crashes.md) |
|
||||
| Inspect native functions, references, or types | [Native binaries](native-investigation.md), [Go metadata](go-binaries.md), [Rust dependencies](rust-binaries.md), [CPython pyc files](python-pyc.md), [PE certificates](pe-certificates.md), [PE imports](pe-imports.md), [PE TLS/load config](pe-load-config.md), [ELF notes](elf-notes.md), [Apple IPS](apple-ips.md), [Erlang BEAM](erlang-beam.md), [offline ELF layout](binary-diagnostics.md), [recorded crashes](recorded-crashes.md) |
|
||||
| Select a native provider or import format | [Ghidra semantics](ghidra-provider.md), [Windows](windows-ghidra-p0.md), [DOS](ghidra-dos.md), [raw bytes](raw-binary.md), [GameCube DOL](ghidra-dol.md), [MIPS](ghidra-mips.md), [PSP](ghidra-psp.md), [NativeAOT](ghidra-nativeaot.md), [IDA](ida-provider.md), [Kuna](kuna-provider.md), [JEB](jeb-analysis.md) |
|
||||
| Analyze JavaScript or Electron artifacts | [Static analysis](javascript-artifact-reconstruction.md), [tracing and comparisons](javascript-application-workflows.md), [application graph](javascript-application-graph.md), [source recovery](javascript-recovery.md) |
|
||||
| Compare static JavaScript with runtime observations | [Runtime attribution](javascript-runtime-observation.md), [reconciliation](javascript-runtime-reconciliation.md), [Electron observation](electron-observation.md), [Electron fuses](electron-fuses.md) |
|
||||
| Inspect assemblies, packages, or firmware | [Godot PCK](godot-pck.md), [.NET](managed-code-analysis.md), [Python packages](python-packaged-applications.md), [Android](android-analysis.md), [APK signatures](apk-signature.md), [Apktool resources](apktool-resource-analysis.md), [ADB acquisition](adb-device-analysis.md), [Apple](apple-application-analysis.md), [Harmony](harmony-analysis.md), [firmware](firmware-analysis.md), [WASM](wasm-artifacts.md), [EVM](evm-bytecode.md) |
|
||||
| Observe or compare a browser scenario | [CDP observation](browser-observation.md), [scenarios](browser-scenario-contract.md), [runtime evidence](web-runtime.md), [historical captures](web-network-captures.md) |
|
||||
| Serve recorded behavior as an agent environment | [Agent environments](agent-environments.md) |
|
||||
| Trace captured website source | [Source locations](web-source-location.md), [module traces](website-module-trace.md), [script export](website-script-export.md) |
|
||||
| Capture and compare a command | [Process capture](process-capture.md) |
|
||||
| Use CLI/MCP and retain evidence | [CLI](cli.md), [MCP contracts](mcp-contracts.md), [agent prompts](mcp-prompts.md), [obligation ledgers](reconstruction-obligation-ledgers.md) |
|
||||
|
||||
For development, use [testing](testing.md), [client verification](client-verification.md),
|
||||
[tool design](tool-design.md), [admission limits and recovery](design/limits-and-recovery.md),
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
# Linux kernel modules
|
||||
|
||||
`inspect_kernel_module` / `inspect-kernel-module` reads metadata from one
|
||||
selected local ELF32/ELF64 relocatable object (`.ko`) without executing it.
|
||||
That metadata is the `.modinfo` key/value strings, the `__versions` symbol
|
||||
CRCs, the presence of `.gnu.linkonce.this_module` and `__ksymtab*` sections,
|
||||
the observed file sizes of `.init.text` and `.exit.text`, and an optional
|
||||
appended signature trailer. It does not require a Linux host or the `modinfo`
|
||||
program.
|
||||
|
||||
```bash
|
||||
rea inspect-kernel-module /absolute/path/to/module.ko --json
|
||||
```
|
||||
|
||||
The CLI resolves relative paths against the caller's working directory. MCP
|
||||
requires an absolute filesystem path and preserves that exact selection:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "inspect_kernel_module",
|
||||
"arguments": { "path": "/artifacts/demo.ko" }
|
||||
}
|
||||
```
|
||||
|
||||
## Observations
|
||||
|
||||
The inline Evidence result contains the artifact path, SHA-256, and byte
|
||||
length. `applicability` is `applicable` only for `ET_REL`. Other ELF types are
|
||||
`not_applicable`: header facts are filled and `.modinfo`, `__versions`, and
|
||||
the signature trailer are not examined.
|
||||
|
||||
`.modinfo` entries stay in file order. Duplicate keys such as `alias` and
|
||||
`parm` are preserved. Each entry keeps its key, value, file range, and
|
||||
SHA-256. Values are the bytes after the first `=`. NUL padding between
|
||||
strings is skipped.
|
||||
|
||||
`__versions` entries are `struct modversion_info`: a 64-byte record whose CRC
|
||||
is an `unsigned long` and whose name occupies the remaining bytes. ELF32
|
||||
stores a 32-bit CRC and a 60-byte name. ELF64 stores a 64-bit unsigned long
|
||||
and a 56-byte name. CRCs are zero-padded lowercase hex, 8 or 16 characters,
|
||||
not a JavaScript number. The layout is reported from the ELF class even when
|
||||
the section is absent.
|
||||
|
||||
`.gnu.linkonce.this_module` is reported as present, absent, or unknown.
|
||||
`layout` is always `unknown` because `struct module` changes with the kernel
|
||||
version and is not decoded. File-backed `__ksymtab*` sections are listed in
|
||||
section-header order by name and file range; their records are not decoded.
|
||||
Sections that are `SHT_NOBITS` or leave the file are counted in one limitation
|
||||
without their names. `.init.text` and `.exit.text` report the
|
||||
observed file-backed size. A compressed payload reports the stored size and a
|
||||
limitation that the decoded size is unknown.
|
||||
|
||||
The signature trailer is the 27-byte magic `~Module signature appended~\n`
|
||||
preceded by a 12-byte `module_signature` (`algo`, `hash`, `id_type`,
|
||||
`signer_len`, `key_id_len`, three pad bytes, and big-endian `sig_len`). When
|
||||
those parameter fields are zero and `id_type` is PKCS#7 (`2`), the preceding
|
||||
`sig_len` bytes are the PKCS#7 range and its SHA-256. The range is not parsed
|
||||
and no certificate, signer, or digest-coverage claim is made. An absent
|
||||
trailer is observed absence, not proof the module is unsigned.
|
||||
|
||||
When section names are incomplete, a missing named section is `unknown`
|
||||
rather than `absent`. An empty file-backed section is present and empty.
|
||||
|
||||
## Limits and failures
|
||||
|
||||
The selected file is limited to 64 MiB by default (512 MiB maximum). At most
|
||||
8192 `.modinfo` and `__versions` entries plus listed `__ksymtab*` sections are
|
||||
examined by default (65536 maximum); `coverage.examined_entries` counts all
|
||||
three. Exceeding that budget fails with no partial success. Keys longer
|
||||
than 256 bytes and values longer than 4096 bytes stay malformed per entry;
|
||||
the field that fits is still copied. A `__ksymtab*` section name longer than
|
||||
256 bytes keeps its range with `name: null` and `ksymtab_name_exceeds_limit`. Inline raw hex stops at 4096 bytes and
|
||||
the SHA-256 remains.
|
||||
|
||||
A `.modinfo` span with no trailing NUL is `unterminated_modinfo_entry` and
|
||||
earlier entries are kept. A string with no `=` is `modinfo_missing_separator`.
|
||||
An empty key is `modinfo_empty_key`. A `__versions` name with no NUL is
|
||||
`version_name_unterminated`. A `__versions` section whose size is not a
|
||||
multiple of 64 bytes is closed with `versions_size_not_multiple` and no
|
||||
entries. `SHT_NOBITS` or a declared range that leaves the file is
|
||||
`modinfo_section_not_file_backed` or `versions_section_not_file_backed`.
|
||||
`SHF_COMPRESSED` payloads are not inflated
|
||||
(`modinfo_section_compressed`, `versions_section_compressed`).
|
||||
|
||||
An appended magic with fewer than 12 preceding bytes is
|
||||
`signature_trailer_truncated`. A `sig_len` that does not leave at least one
|
||||
module byte is `signature_length_inconsistent`. Nonzero `algo`, `hash`,
|
||||
signer, key-id, or pad fields for PKCS#7 are `signature_parameters_nonzero`.
|
||||
A parameter block whose `id_type` is not PKCS#7 is
|
||||
`signature_id_type_not_pkcs7` and is unsupported rather than a validated
|
||||
signature.
|
||||
|
||||
gzip (`1f 8b`), xz (`fd 37 7a 58 5a 00`), and zstd (`28 b5 2f fd`) whole-file
|
||||
containers are `unsupported`. The result names `external_decompression` as the
|
||||
next probe and does not claim the payload is a kernel module. This is an external
|
||||
step, not a REA operation: neither this tool nor `extract_artifact` decodes these
|
||||
containers. Use an external gzip, xz, or zstd utility to decompress into a separate
|
||||
file, then call `inspect_kernel_module` on that file to inspect the `ET_REL` image.
|
||||
|
||||
Malformed ELF headers return no partial kernel-module metadata. This workflow
|
||||
does not execute the module or validate an appended signature.
|
||||
|
||||
## Format references
|
||||
|
||||
The record layout follows Linux v6.12's
|
||||
[`modversion_info`](https://github.com/torvalds/linux/blob/v6.12/include/linux/module.h)
|
||||
and [`MAX_PARAM_PREFIX_LEN`](https://github.com/torvalds/linux/blob/v6.12/include/linux/moduleparam.h).
|
||||
Trailer fields and acceptance rules follow
|
||||
[`module_signature`](https://github.com/torvalds/linux/blob/v6.12/include/linux/module_signature.h)
|
||||
and [`mod_check_sig`](https://github.com/torvalds/linux/blob/v6.12/kernel/module_signature.c).
|
||||
These references establish the decoded layout, not runtime verification.
|
||||
|
||||
## Real-module verification lane
|
||||
|
||||
Run from a built checkout with Docker available. This downloads a Debian kernel
|
||||
package into a disposable container, extracts a filesystem module, decompresses
|
||||
it when necessary, and inspects it with REA. It never installs or loads that
|
||||
kernel module. The temporary output is outside the repository; record the
|
||||
printed package version and module digest with results.
|
||||
|
||||
```bash
|
||||
module_lane=$(mktemp -d)
|
||||
docker run --rm -i -v "$module_lane:/out" debian:stable bash -se <<'CONTAINER'
|
||||
apt-get update -qq
|
||||
apt-get install -y --no-install-recommends xz-utils zstd ca-certificates
|
||||
cd /tmp
|
||||
arch=$(dpkg --print-architecture)
|
||||
package=$(apt-cache depends "linux-image-$arch" | awk '/Depends: linux-image-[0-9]/{print $2; exit}')
|
||||
test -n "$package"
|
||||
apt-get download "$package"
|
||||
dpkg-deb -f ./*.deb Package Version > /out/package.txt
|
||||
dpkg-deb -x ./*.deb /tmp/kernel
|
||||
module=$(find /tmp/kernel -type f -path '*/kernel/fs/*' \( -name '*.ko' -o -name '*.ko.xz' -o -name '*.ko.zst' -o -name '*.ko.gz' \) | sort | head -1)
|
||||
test -n "$module"
|
||||
case "$module" in
|
||||
*.xz) xz -dc "$module" > /out/module.ko ;;
|
||||
*.zst) zstd -dc "$module" > /out/module.ko ;;
|
||||
*.gz) gzip -dc "$module" > /out/module.ko ;;
|
||||
*) cp "$module" /out/module.ko ;;
|
||||
esac
|
||||
cat /out/package.txt
|
||||
sha256sum /out/module.ko
|
||||
CONTAINER
|
||||
node scripts/rea.mjs inspect-kernel-module "$module_lane/module.ko" --json > "$module_lane/rea.json"
|
||||
python3 - "$module_lane" <<'PY'
|
||||
import json, pathlib, sys
|
||||
root = pathlib.Path(sys.argv[1])
|
||||
report = json.loads((root / 'rea.json').read_text())['normalized_result']
|
||||
entries = report['modinfo']['entries']
|
||||
assert entries and all(entry['status'] == 'observed' for entry in entries)
|
||||
keys = [entry['key'] for entry in entries]
|
||||
for key in ('license', 'vermagic', 'name'):
|
||||
assert key in keys, key
|
||||
print('Observed .modinfo entries:', len(entries), 'distinct keys:', len(set(keys)))
|
||||
print('Artifact:', report['artifact'])
|
||||
PY
|
||||
```
|
||||
|
||||
`parm` and `parmtype` are retained separately by REA. Synthetic tests cover
|
||||
their generic key/value representation and all four ELF class/byte-order
|
||||
combinations. A successful real lane verifies only the selected module and
|
||||
printed fields; it does not validate certificates. This lane has not run in the
|
||||
sandbox used for implementation because access to the Docker socket was denied.
|
||||
@@ -158,6 +158,7 @@ npx rea-agents@latest setup
|
||||
| شيفرة EVM البايتية | محددات التوجيه، وإزاحات البايتات، والمعاملات المستنتجة، وقابلية تغيير الحالة | مدخل محلي يحمل بايتات خامًا أو تمثيلًا سداسيًا عشريًا على Linux x64؛ [دليل EVM دون تشغيل](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Linux kernel modules | .modinfo keys, __versions CRCs, this_module presence, and signature trailer | Built-in static ELF inspection; [kernel modules](../kernel-modules.md) |
|
||||
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
|
||||
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
|
||||
| أعطال Linux المسجّلة | سجلات note الخام، ومسجّلات المعالج/الإشارات لكل خيط مسجّل، وترشيحات اختيارية لخرائط الذاكرة | pwntools يوفّره المستدعي على Linux x64؛ GDB/pwndbg اختياريان؛ [الأعطال المسجّلة](../recorded-crashes.md) |
|
||||
|
||||
@@ -158,6 +158,7 @@ REA benötigt Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) oder 26+ (>=26.12.1) so
|
||||
| EVM-Bytecode | Dispatch-Selektoren, Byte-Offsets, abgeleitete Argumente und Veränderbarkeit des Zustands | Lokale Eingabe mit Rohbytes oder Hexdarstellung unter Linux x64; [Offline-EVM-Leitfaden](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Linux kernel modules | .modinfo keys, __versions CRCs, this_module presence, and signature trailer | Built-in static ELF inspection; [kernel modules](../kernel-modules.md) |
|
||||
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
|
||||
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
|
||||
| Aufgezeichnete Linux-Abstürze | Unverarbeitete Note-Einträge, Register/Signale aller aufgezeichneten Threads und optionale Zuordnungskandidaten | Vom Aufrufer bereitgestelltes pwntools unter Linux x64; optional GDB/pwndbg; [aufgezeichnete Abstürze](../recorded-crashes.md) |
|
||||
|
||||
@@ -159,6 +159,7 @@ npx rea-agents@latest setup
|
||||
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
|
||||
| Linux kernel modules | .modinfo keys, __versions CRCs, this_module presence, and signature trailer | Built-in static ELF inspection; [kernel modules](../kernel-modules.md) |
|
||||
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
|
||||
| Αρχείο WASM | Έλεγχο εγκυρότητας, ενότητες, ακριβείς μορφές εισαγωγών/εξαγωγών σε WAT και αποκωδικοποιημένο WAT | WABT 1.0.42 που παρέχει ο χρήστης· `inspect_wasm_artifact` |
|
||||
| Bytecode EVM | Selectors επιλογής συνάρτησης, μετατοπίσεις σε bytes, εκτιμώμενα ορίσματα και δυνατότητα μεταβολής της κατάστασης | Τοπική είσοδος σε ακατέργαστα bytes ή δεκαεξαδική μορφή σε Linux x64· [οδηγός EVM χωρίς εκτέλεση](../evm-bytecode.md) |
|
||||
|
||||
@@ -158,6 +158,7 @@ REA requiere Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) o 26+ (>=26.12.1), adem
|
||||
| Bytecode EVM | Selectores de despacho, desplazamientos en bytes, argumentos inferidos y mutabilidad | Entrada local con bytes sin procesar o representación hexadecimal, en Linux x64; [guía de EVM sin ejecutar](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Linux kernel modules | .modinfo keys, __versions CRCs, this_module presence, and signature trailer | Built-in static ELF inspection; [kernel modules](../kernel-modules.md) |
|
||||
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
|
||||
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
|
||||
| Fallos de Linux registrados | Registros note sin procesar, registros/señales de cada hilo registrado y posibles correspondencias de memoria opcionales | pwntools proporcionado por quien llama, en Linux x64; GDB/pwndbg opcionales; [fallos registrados](../recorded-crashes.md) |
|
||||
|
||||
@@ -151,6 +151,7 @@ REA به Node.js 22.x (>=22.23.4)، 24.x (>=24.22.1) یا 26+ (>=26.12.1)، به
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
|
||||
| Linux kernel modules | .modinfo keys, __versions CRCs, this_module presence, and signature trailer | Built-in static ELF inspection; [kernel modules](../kernel-modules.md) |
|
||||
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
|
||||
| کرشهای ثبتشدهٔ لینوکس | یادداشتهای خام، رجیسترها/سیگنالهای تمام threadهای ثبتشده و گزینههای احتمالی نگاشت | pwntools تأمینشده توسط فراخواننده روی Linux x64؛ GDB/pwndbg اختیاری؛ [کرشهای ثبتشده](../recorded-crashes.md) |
|
||||
| نصبهای دسکتاپ | نامزدهای زمان اجرا، فایلهای شاهد، نشانههای مؤلفهها و مراحل بعدی | بررسی محدود پوشهها و سرآیندها;[برنامههای دسکتاپ](../desktop-applications.md) |
|
||||
|
||||
@@ -158,6 +158,7 @@ REA nécessite Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) ou 26+ (>=26.12.1), ai
|
||||
| Bytecode EVM | Sélecteurs de dispatch, offsets en octets, arguments inférés et mutabilité | Fichier local brut ou hexadécimal sous Linux x64 ; [guide EVM hors ligne](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Linux kernel modules | .modinfo keys, __versions CRCs, this_module presence, and signature trailer | Built-in static ELF inspection; [kernel modules](../kernel-modules.md) |
|
||||
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
|
||||
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
|
||||
| Crashs Linux enregistrés | Notes brutes, registres et signaux de chaque thread enregistré, et correspondances de mappings facultatives | pwntools fourni par l'utilisateur sous Linux x64 ; GDB/pwndbg en option ; [crashs enregistrés](../recorded-crashes.md) |
|
||||
|
||||
@@ -162,6 +162,7 @@ Alat tambahan dan dukungan sistem host bergantung pada target yang dianalisis:
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
|
||||
| Linux kernel modules | .modinfo keys, __versions CRCs, this_module presence, and signature trailer | Built-in static ELF inspection; [kernel modules](../kernel-modules.md) |
|
||||
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
|
||||
| Crash Linux yang direkam | Catatan mentah, register/sinyal setiap thread yang direkam, dan kandidat pemetaan opsional | pwntools yang disediakan pemanggil; GDB/pwndbg opsional; [crash yang direkam](../recorded-crashes.md) |
|
||||
| Instalasi desktop | Kandidat runtime, berkas pendukung, penanda komponen, dan langkah berikutnya | Pemeriksaan direktori dan header terbatas;[Aplikasi desktop](../desktop-applications.md) |
|
||||
|
||||
@@ -158,6 +158,7 @@ REA richiede Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) o 26+ (>=26.12.1), oltre
|
||||
| Bytecode EVM | Selettori di dispatch, offset dei byte, argomenti inferiti e mutabilità | Carrier raw/hex locale; [guida EVM offline](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Linux kernel modules | .modinfo keys, __versions CRCs, this_module presence, and signature trailer | Built-in static ELF inspection; [kernel modules](../kernel-modules.md) |
|
||||
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
|
||||
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
|
||||
| Crash Linux registrati | Note grezze, registri/segnai di ogni thread registrato e candidati mapping opzionali | pwntools fornito dal chiamante; GDB/pwndbg opzionale; [crash registrati](../recorded-crashes.md) |
|
||||
|
||||
@@ -159,6 +159,7 @@ REA には Node.js 22.x(>=22.23.4)、24.x(>=24.22.1)、または 26+ (>=
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
|
||||
| Linux kernel modules | .modinfo keys, __versions CRCs, this_module presence, and signature trailer | Built-in static ELF inspection; [kernel modules](../kernel-modules.md) |
|
||||
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
|
||||
| 記録済みの Linux クラッシュ | 生の note レコード、記録された全スレッドのレジスター/シグナル、任意のマッピング候補 | Linux x64 で利用者が用意する pwntools;GDB/pwndbg は任意;[記録済みクラッシュ](../recorded-crashes.md) |
|
||||
| デスクトップのインストール先 | ランタイム候補、根拠ファイル、コンポーネントの目印、次の調査手順 | 上限付きのディレクトリ・ヘッダー検査;[デスクトップアプリ](../desktop-applications.md) |
|
||||
|
||||
@@ -159,6 +159,7 @@ REA에는 Node.js 22.x(>=22.23.4), 24.x(>=24.22.1) 또는 26+ (>=26.12.1)와 npm
|
||||
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
|
||||
| Linux kernel modules | .modinfo keys, __versions CRCs, this_module presence, and signature trailer | Built-in static ELF inspection; [kernel modules](../kernel-modules.md) |
|
||||
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
|
||||
| WASM 아티팩트 | 유효성 검사, 섹션, import/export의 정확한 WAT 형식, 디코딩된 WAT | 호출자 제공 WABT 1.0.42; `inspect_wasm_artifact` |
|
||||
| EVM 바이트코드 | 디스패치 선택자, 바이트 오프셋, 추론한 인자와 상태 변경 가능 여부 | Linux x64의 로컬 원시 바이트/16진수 입력 파일; [오프라인 EVM 가이드](../evm-bytecode.md) |
|
||||
|
||||
@@ -158,6 +158,7 @@ REA-യ്ക്ക് Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), അല്ല
|
||||
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
|
||||
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Linux kernel modules | .modinfo keys, __versions CRCs, this_module presence, and signature trailer | Built-in static ELF inspection; [kernel modules](../kernel-modules.md) |
|
||||
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
|
||||
| WASM artifact | Validation, sections, കൃത്യമായ import/export WAT രൂപങ്ങൾ, decoded WAT | Caller നൽകുന്ന WABT 1.0.42; `inspect_wasm_artifact` |
|
||||
| EVM bytecode | Dispatch selectors, byte offsets, അനുമാനിച്ച arguments, mutability | Linux x64-ൽ local raw/hex carrier; [offline EVM guide](../evm-bytecode.md) |
|
||||
|
||||
@@ -158,6 +158,7 @@ REA wymaga Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) lub 26+ (>=26.12.1) oraz n
|
||||
| Kod bajtowy EVM | Selektory wywołań, przesunięcia bajtowe, wywnioskowane argumenty i mutowalność | Lokalny plik z surowymi bajtami lub zapisem szesnastkowym w systemie Linux x64; [przewodnik EVM offline](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Linux kernel modules | .modinfo keys, __versions CRCs, this_module presence, and signature trailer | Built-in static ELF inspection; [kernel modules](../kernel-modules.md) |
|
||||
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
|
||||
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
|
||||
| Zarejestrowane awarie systemu Linux | Surowe rekordy note, rejestry/sygnały każdego zarejestrowanego wątku i opcjonalni kandydaci na mapowania | pwntools dostarczone przez użytkownika w systemie Linux x64; opcjonalnie GDB/pwndbg; [zarejestrowane awarie](../recorded-crashes.md) |
|
||||
|
||||
@@ -158,6 +158,7 @@ O REA requer Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) ou 26+ (>=26.12.1), alé
|
||||
| Bytecode EVM | Seletores de despacho, offsets de bytes, argumentos inferidos e mutabilidade | Entrada local com bytes brutos ou representação hexadecimal no Linux x64; [guia de EVM sem execução](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Linux kernel modules | .modinfo keys, __versions CRCs, this_module presence, and signature trailer | Built-in static ELF inspection; [kernel modules](../kernel-modules.md) |
|
||||
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
|
||||
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
|
||||
| Falhas do Linux registradas | Registros note brutos, registradores/sinais de cada thread registrada e candidatos opcionais a mapeamentos | pwntools fornecido pelo chamador no Linux x64; GDB/pwndbg opcionais; [falhas registradas](../recorded-crashes.md) |
|
||||
|
||||
@@ -158,6 +158,7 @@ REA требует Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) или 26+ (>=26.1
|
||||
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
|
||||
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Linux kernel modules | .modinfo keys, __versions CRCs, this_module presence, and signature trailer | Built-in static ELF inspection; [kernel modules](../kernel-modules.md) |
|
||||
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
|
||||
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
|
||||
| Записанные сбои Linux | Необработанные записи note, регистры/сигналы каждого записанного потока и необязательные кандидаты сопоставления с памятью | pwntools, предоставленный вызывающей стороной, в Linux x64; необязательные GDB/pwndbg; [записанные сбои](../recorded-crashes.md) |
|
||||
|
||||
@@ -158,6 +158,7 @@ REA ต้องใช้ Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) หรื
|
||||
| ไบต์โค้ด EVM | ตัวเลือกฟังก์ชันสำหรับการเรียก ออฟเซ็ตไบต์ อาร์กิวเมนต์และความสามารถในการเปลี่ยนสถานะที่อนุมานได้ | ไฟล์บนเครื่องที่มีไบต์ดิบหรือข้อมูลเลขฐานสิบหกบน Linux x64; [คู่มือ EVM แบบออฟไลน์](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Linux kernel modules | .modinfo keys, __versions CRCs, this_module presence, and signature trailer | Built-in static ELF inspection; [kernel modules](../kernel-modules.md) |
|
||||
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
|
||||
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
|
||||
| ข้อมูลการแครชของ Linux ที่บันทึกไว้ | ระเบียน note ดิบ รีจิสเตอร์/สัญญาณของทุกเธรดที่บันทึกไว้ และตัวเลือกการแมปเพิ่มเติม | pwntools ที่ผู้ใช้จัดเตรียมบน Linux x64; เลือกใช้ GDB/pwndbg เพิ่มเติมได้; [การแครชที่บันทึกไว้](../recorded-crashes.md) |
|
||||
|
||||
@@ -157,6 +157,7 @@ REA; Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) veya 26+ (>=26.12.1) ve npm gere
|
||||
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
|
||||
| Linux kernel modules | .modinfo keys, __versions CRCs, this_module presence, and signature trailer | Built-in static ELF inspection; [kernel modules](../kernel-modules.md) |
|
||||
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
|
||||
| WASM artefaktı | Doğrulama, bölümler, tam import/export WAT biçimleri ve decode edilmiş WAT | Çağıranın sağladığı WABT 1.0.42; `inspect_wasm_artifact` |
|
||||
| EVM bayt kodu | Yönlendirme seçicileri, bayt ofsetleri, çıkarımla belirlenen argümanlar ve durum değiştirilebilirliği | Linux x64 üzerinde yerel ham bayt/onaltılık girdi; [çevrimdışı EVM rehberi](../evm-bytecode.md) |
|
||||
|
||||
@@ -158,6 +158,7 @@ REA потребує Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) або 26+ (>=26
|
||||
| Байт-код EVM | Селектори диспетчеризації, зміщення байтів, виведені припущення про аргументи та змінюваність стану | Локальні необроблені байти або шістнадцяткове представлення у Linux x64; [посібник EVM без запуску](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Linux kernel modules | .modinfo keys, __versions CRCs, this_module presence, and signature trailer | Built-in static ELF inspection; [kernel modules](../kernel-modules.md) |
|
||||
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
|
||||
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
|
||||
| Записані аварійні завершення в Linux | Необроблені записи note, регістри/сигнали кожного записаного потоку й необов'язкові кандидати зіставлення пам'яті | pwntools, наданий стороною, що викликає інструмент, у Linux x64; необов'язкові GDB/pwndbg; [записані аварійні завершення](../recorded-crashes.md) |
|
||||
|
||||
@@ -198,6 +198,7 @@ Mazeed tools aur host support target par depend karte hain:
|
||||
| PE certificates | Authenticode WIN_CERTIFICATE entries aur PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
|
||||
| PE imports | Import aur delay-load DLL names, ordinals, IAT RVAs aur pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id aur ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Linux kernel modules | .modinfo keys, __versions CRCs, this_module presence, and signature trailer | Built-in static ELF inspection; [kernel modules](../kernel-modules.md) |
|
||||
| Erlang BEAM | IFF chunks, atoms, exports, imports, lambdas aur Code header | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
|
||||
| WASM artifact | Validation, sections, exact import/export WAT forms aur decoded WAT | Aapka diya hua WABT 1.0.42; `inspect_wasm_artifact` |
|
||||
| EVM bytecode | Dispatch selectors, byte offsets, inferred arguments aur mutability | Linux x64 par local raw/hex carrier; [offline EVM guide](../evm-bytecode.md) |
|
||||
|
||||
@@ -158,6 +158,7 @@ REA yêu cầu Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) hoặc 26+ (>=26.12.1)
|
||||
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
|
||||
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Linux kernel modules | .modinfo keys, __versions CRCs, this_module presence, and signature trailer | Built-in static ELF inspection; [kernel modules](../kernel-modules.md) |
|
||||
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
|
||||
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
|
||||
| Sự cố Linux đã ghi lại | Bản ghi note thô, thanh ghi/tín hiệu của mọi luồng được ghi lại và các ứng viên ánh xạ tùy chọn | pwntools do bên gọi cung cấp trên Linux x64; GDB/pwndbg tùy chọn; [sự cố đã ghi lại](../recorded-crashes.md) |
|
||||
|
||||
@@ -160,6 +160,7 @@ REA 需要 Node.js 22.x(>=22.23.4)、24.x(>=24.22.1)或 26+ (>=26.12.1)
|
||||
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
|
||||
| Linux kernel modules | .modinfo keys, __versions CRCs, this_module presence, and signature trailer | Built-in static ELF inspection; [kernel modules](../kernel-modules.md) |
|
||||
| 已記錄的 Linux 崩潰 | 原始 note 記錄、每個已記錄執行緒的暫存器/訊號及可選的對映候選項 | Linux x64 上由呼叫方提供的 pwntools;可選的 GDB/pwndbg;[已記錄崩潰](../recorded-crashes.md) |
|
||||
| 桌面安裝目錄 | 執行階段候選、支持檔案、元件標記與後續操作 | 有界的目錄與檔頭檢查;[桌面應用](../desktop-applications.md) |
|
||||
| JavaScript / Electron | 模組、匯入、source map、路由、IPC 和原生附加元件關係 | Node.js 和 npm;[應用分析](../javascript-application-workflows.md); Linux: procps `ps` |
|
||||
|
||||
@@ -160,6 +160,7 @@ REA 需要 Node.js 22.x(>=22.23.4)、24.x(>=24.22.1)或 26+ (>=26.12.1)
|
||||
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) |
|
||||
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
|
||||
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
|
||||
| Linux kernel modules | .modinfo keys, __versions CRCs, this_module presence, and signature trailer | Built-in static ELF inspection; [kernel modules](../kernel-modules.md) |
|
||||
| 已记录的 Linux 崩溃 | 原始 note 记录、每个已记录线程的寄存器/信号及可选的映射候选项 | Linux x64 上由调用方提供的 pwntools;可选的 GDB/pwndbg;[已记录崩溃](../recorded-crashes.md) |
|
||||
| 桌面安装目录 | 运行时候选、支持文件、组件标记和后续操作 | 有界的目录与文件头检查;[桌面应用](../desktop-applications.md) |
|
||||
| JavaScript / Electron | 模块、导入、source map、路由、IPC 和原生扩展关系 | Node.js 和 npm;[应用分析](../javascript-application-workflows.md); Linux: procps `ps` |
|
||||
|
||||
@@ -289,6 +289,13 @@ export const providerCatalog = (sources) => {
|
||||
({ name }) => name === "inspect_apple_ips",
|
||||
),
|
||||
},
|
||||
{
|
||||
identity: sources.artifactProviders.KERNEL_MODULE_PROVIDER,
|
||||
contracts:
|
||||
sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter(
|
||||
({ name }) => name === "inspect_kernel_module",
|
||||
),
|
||||
},
|
||||
{
|
||||
identity: sources.artifactProviders.BYTE_ENTROPY_PROVIDER,
|
||||
contracts:
|
||||
|
||||
@@ -532,7 +532,11 @@ const workflowAvailabilityFor = ({
|
||||
return { reason: "available", remediation: null };
|
||||
if (name === "inspect_pe_certificates" || name === "inspect_pe_imports")
|
||||
return { reason: "available", remediation: null };
|
||||
if (name === "inspect_elf_notes" || name === "inspect_pe_load_config")
|
||||
if (
|
||||
name === "inspect_elf_notes" ||
|
||||
name === "inspect_kernel_module" ||
|
||||
name === "inspect_pe_load_config"
|
||||
)
|
||||
return { reason: "available", remediation: null };
|
||||
if (name === "inspect_evm_interface") {
|
||||
const profile = hostRestrictedAvailability(
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { ElfNotesService } from "./ElfNotesService.js";
|
||||
import { KernelModuleService } from "./KernelModuleService.js";
|
||||
import { PeCertificatesService } from "./PeCertificatesService.js";
|
||||
import { PeImportsService } from "./PeImportsService.js";
|
||||
import {
|
||||
ELF_NOTES_PROVIDER,
|
||||
KERNEL_MODULE_PROVIDER,
|
||||
PE_CERTIFICATES_PROVIDER,
|
||||
PE_IMPORTS_PROVIDER,
|
||||
} from "../../domain/investigationProviders.js";
|
||||
@@ -29,6 +31,11 @@ it.each([
|
||||
"ELF notes",
|
||||
() => new ElfNotesService({ identity: ELF_NOTES_PROVIDER, inspect }),
|
||||
],
|
||||
[
|
||||
"kernel module",
|
||||
() =>
|
||||
new KernelModuleService({ identity: KERNEL_MODULE_PROVIDER, inspect }),
|
||||
],
|
||||
] as const)(
|
||||
"preserves an early cancellation reason for %s before validation",
|
||||
async (_label, create) => {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import type {
|
||||
ExecutionOptions,
|
||||
ProviderIdentity,
|
||||
} from "../AnalysisProvider.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import type {
|
||||
InspectKernelModuleInput,
|
||||
KernelModule,
|
||||
} from "../../domain/native/kernelModule.js";
|
||||
import type { Result } from "../../domain/result.js";
|
||||
|
||||
/** Stable read and kernel-module metadata decoding, independent of a disassembler session. */
|
||||
export interface KernelModulePort {
|
||||
readonly identity: ProviderIdentity;
|
||||
close?(): Promise<void>;
|
||||
inspect(
|
||||
input: InspectKernelModuleInput,
|
||||
options?: ExecutionOptions,
|
||||
): Promise<Result<KernelModule, AnalysisError>>;
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { KernelModuleService } from "./KernelModuleService.js";
|
||||
import { KERNEL_MODULE_PROVIDER } from "../../domain/investigationProviders.js";
|
||||
import type { KernelModule } from "../../domain/native/kernelModule.js";
|
||||
import { ok } from "../../domain/result.js";
|
||||
import { parseEvidence } from "../../domain/evidence.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisInputError,
|
||||
AnalysisOutputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
|
||||
const identity = KERNEL_MODULE_PROVIDER;
|
||||
const digest = "a".repeat(64);
|
||||
const report: KernelModule = {
|
||||
artifact: { path: "/artifacts/demo.ko", sha256: digest, bytes: 512 },
|
||||
format: "elf64",
|
||||
byte_order: "little",
|
||||
machine: 62,
|
||||
type: 1,
|
||||
type_name: "relocatable",
|
||||
applicability: "applicable",
|
||||
compression: null,
|
||||
next_probe: null,
|
||||
modinfo: {
|
||||
status: "present",
|
||||
location: { offset: 64, bytes: 0 },
|
||||
sha256: digest,
|
||||
entries: [],
|
||||
reason: null,
|
||||
},
|
||||
versions: {
|
||||
status: "absent",
|
||||
layout: "elf64_unsigned_long_name56",
|
||||
entry_bytes: 64,
|
||||
location: null,
|
||||
sha256: null,
|
||||
entries: [],
|
||||
reason: null,
|
||||
},
|
||||
this_module: {
|
||||
status: "absent",
|
||||
location: null,
|
||||
layout: "unknown",
|
||||
},
|
||||
ksymtab: { status: "observed", sections: [] },
|
||||
init_text: {
|
||||
status: "absent",
|
||||
location: null,
|
||||
bytes: null,
|
||||
compressed: null,
|
||||
},
|
||||
exit_text: {
|
||||
status: "absent",
|
||||
location: null,
|
||||
bytes: null,
|
||||
compressed: null,
|
||||
},
|
||||
signature: {
|
||||
status: "absent",
|
||||
magic: null,
|
||||
info: null,
|
||||
pkcs7: null,
|
||||
reason: null,
|
||||
},
|
||||
coverage: { status: "complete", examined_entries: 0 },
|
||||
limitations: ["Kernel module metadata was not executed."],
|
||||
};
|
||||
|
||||
it("rejects a relative path and extra arguments before reading", async () => {
|
||||
let calls = 0;
|
||||
const service = new KernelModuleService({
|
||||
identity,
|
||||
inspect: () => {
|
||||
calls += 1;
|
||||
return Promise.resolve(ok(report));
|
||||
},
|
||||
});
|
||||
const relative = await service.inspect({ path: "demo.ko" });
|
||||
const extra = await service.inspect({
|
||||
path: report.artifact.path,
|
||||
extra: true,
|
||||
});
|
||||
expect(relative.ok).toBe(false);
|
||||
expect(extra.ok).toBe(false);
|
||||
if (!relative.ok) expect(relative.error).toBeInstanceOf(AnalysisInputError);
|
||||
if (!extra.ok) expect(extra.error).toBeInstanceOf(AnalysisInputError);
|
||||
expect(calls).toBe(0);
|
||||
});
|
||||
|
||||
it("records an applicable module and its x86_64 architecture", async () => {
|
||||
const result = await new KernelModuleService({
|
||||
identity,
|
||||
inspect: () => Promise.resolve(ok(report)),
|
||||
}).inspect({ path: report.artifact.path });
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value).toMatchObject({
|
||||
confidence: "observed",
|
||||
subject: { format: "elf", architecture: "x86_64" },
|
||||
normalized_result: report,
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects a report whose entry count does not match the examined sections", async () => {
|
||||
const result = await new KernelModuleService({
|
||||
identity,
|
||||
inspect: () =>
|
||||
Promise.resolve(
|
||||
ok({
|
||||
...report,
|
||||
coverage: { status: "complete", examined_entries: 1 },
|
||||
}),
|
||||
),
|
||||
}).inspect({ path: report.artifact.path });
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisOutputError);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["__ksymtab", "ksymtab_name_exceeds_limit"],
|
||||
[null, null],
|
||||
] as const)(
|
||||
"rejects a __ksymtab listing whose name %s disagrees with reason %s",
|
||||
async (name, reason) => {
|
||||
const result = await new KernelModuleService({
|
||||
identity,
|
||||
inspect: () =>
|
||||
Promise.resolve(
|
||||
ok({
|
||||
...report,
|
||||
ksymtab: {
|
||||
status: "observed",
|
||||
sections: [
|
||||
{
|
||||
name,
|
||||
location: { offset: 64, bytes: 0 },
|
||||
compressed: false,
|
||||
reason,
|
||||
},
|
||||
],
|
||||
},
|
||||
coverage: { status: "complete", examined_entries: 1 },
|
||||
}),
|
||||
),
|
||||
}).inspect({ path: report.artifact.path });
|
||||
if (result.ok) throw new Error("Expected malformed provider output");
|
||||
expect(result.error).toBeInstanceOf(AnalysisOutputError);
|
||||
},
|
||||
);
|
||||
|
||||
it("retains a completed observation if the caller cancels while the provider completes", async () => {
|
||||
const controller = new AbortController();
|
||||
const reason = { request: "late kernel module cancellation" };
|
||||
const service = new KernelModuleService({
|
||||
identity,
|
||||
inspect: () => {
|
||||
controller.abort(reason);
|
||||
return Promise.resolve(ok(report));
|
||||
},
|
||||
});
|
||||
const late = await service.inspect(
|
||||
{ path: report.artifact.path },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
expect(late).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisCancelledError" },
|
||||
});
|
||||
if (late.ok) throw new Error("Expected late cancellation");
|
||||
expect(late.error).toBeInstanceOf(AnalysisCancelledError);
|
||||
expect(late.error.cause).toBe(reason);
|
||||
expect(parseEvidence(late.error.partialObservation)).toMatchObject({
|
||||
operation: "inspect_kernel_module",
|
||||
confidence: "observed",
|
||||
subject: { format: "elf", architecture: "x86_64" },
|
||||
normalized_result: report,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,369 @@
|
||||
import { isAbsolute } from "node:path";
|
||||
import type { ExecutionOptions } from "../AnalysisProvider.js";
|
||||
import type { KernelModulePort } from "./KernelModulePort.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisInputError,
|
||||
AnalysisOutputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import { elfObjectTypeName } from "../../domain/native/elfNotes.js";
|
||||
import {
|
||||
inspectKernelModuleInputSchema,
|
||||
kernelModuleSchema,
|
||||
KERNEL_MODULE_VERSION_ENTRY_BYTES,
|
||||
MAX_KERNEL_MODULE_RAW_BYTES,
|
||||
MODULE_SIGNATURE_INFO_BYTES,
|
||||
MODULE_SIGNATURE_MAGIC,
|
||||
MODULE_SIGNATURE_PKCS7_ID_TYPE,
|
||||
type KernelModule,
|
||||
} from "../../domain/native/kernelModule.js";
|
||||
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
|
||||
import { BINARY_ARCHITECTURES } from "../../domain/binaryTargetTypes.js";
|
||||
import { elfArchitecture } from "../../domain/elfFixedHeader.js";
|
||||
import { err, type Result } from "../../domain/result.js";
|
||||
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
|
||||
|
||||
const OPERATION = "inspect_kernel_module";
|
||||
|
||||
type Range = { readonly offset: number; readonly bytes: number };
|
||||
|
||||
/** Shared caller validation, output parsing and Evidence provenance for CLI/MCP. */
|
||||
export class KernelModuleService {
|
||||
constructor(readonly provider: KernelModulePort) {}
|
||||
|
||||
close(): Promise<void> {
|
||||
return this.provider.close?.() ?? Promise.resolve();
|
||||
}
|
||||
|
||||
async inspect(
|
||||
rawInput: unknown,
|
||||
options?: ExecutionOptions,
|
||||
): Promise<Result<Evidence, AnalysisError>> {
|
||||
if (options?.signal?.aborted)
|
||||
return err(
|
||||
new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }),
|
||||
);
|
||||
const input = inspectKernelModuleInputSchema.safeParse(rawInput);
|
||||
if (!input.success)
|
||||
return err(
|
||||
analysisInputErrorFromIssues(OPERATION, input.error.issues, rawInput, {
|
||||
cause: input.error,
|
||||
}),
|
||||
);
|
||||
if (!isAbsolute(input.data.path))
|
||||
return err(
|
||||
new AnalysisInputError(OPERATION, undefined, [
|
||||
{
|
||||
path: ["path"],
|
||||
reason: "invalid_format",
|
||||
message: "Expected an absolute filesystem path on this host.",
|
||||
},
|
||||
]),
|
||||
);
|
||||
const result = await this.provider.inspect(input.data, options);
|
||||
if (!result.ok) return result;
|
||||
const report = kernelModuleSchema.safeParse(result.value);
|
||||
if (
|
||||
!report.success ||
|
||||
report.data.artifact.path !== input.data.path ||
|
||||
!validReport(
|
||||
report.data,
|
||||
input.data.max_entries,
|
||||
input.data.max_file_bytes,
|
||||
)
|
||||
)
|
||||
return err(
|
||||
new AnalysisOutputError(
|
||||
OPERATION,
|
||||
"Kernel module provider returned malformed output or changed artifact identity.",
|
||||
),
|
||||
);
|
||||
const value = report.data;
|
||||
const machine = value.machine;
|
||||
const mapped = machine === null ? undefined : elfArchitecture(machine);
|
||||
const architecture = BINARY_ARCHITECTURES.find(
|
||||
(supported) => supported === mapped,
|
||||
);
|
||||
const locations = [
|
||||
{ kind: "artifact-path" as const, path: value.artifact.path },
|
||||
...rangesOf(value).map((range) => ({
|
||||
kind: "file-offset-range" as const,
|
||||
start: range.offset,
|
||||
end: range.offset + range.bytes,
|
||||
})),
|
||||
];
|
||||
return completeProviderEvidence(
|
||||
createEvidence(
|
||||
{
|
||||
path: value.artifact.path,
|
||||
format: value.applicability === "unsupported" ? "file" : "elf",
|
||||
sha256: value.artifact.sha256,
|
||||
...(architecture === undefined ? {} : { architecture }),
|
||||
},
|
||||
this.provider.identity,
|
||||
{
|
||||
operation: OPERATION,
|
||||
parameters: input.data,
|
||||
result: value,
|
||||
confidence: "observed",
|
||||
limitations: value.limitations,
|
||||
locations,
|
||||
},
|
||||
),
|
||||
options?.signal,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const inside =
|
||||
(fileBytes: number) =>
|
||||
(range: Range): boolean =>
|
||||
range.bytes <= fileBytes && range.offset <= fileBytes - range.bytes;
|
||||
|
||||
const rangesOf = (report: KernelModule): Range[] => {
|
||||
const found: Range[] = [];
|
||||
const add = (range: Range | null | undefined): void => {
|
||||
if (range !== null && range !== undefined) found.push(range);
|
||||
};
|
||||
add(report.modinfo?.location);
|
||||
for (const entry of report.modinfo?.entries ?? []) add(entry.location);
|
||||
add(report.versions?.location);
|
||||
for (const entry of report.versions?.entries ?? []) add(entry.location);
|
||||
add(report.this_module?.location);
|
||||
add(report.init_text?.location);
|
||||
add(report.exit_text?.location);
|
||||
for (const section of report.ksymtab?.sections ?? []) add(section.location);
|
||||
add(report.signature?.magic);
|
||||
add(report.signature?.info?.location);
|
||||
add(report.signature?.pkcs7?.location);
|
||||
return found;
|
||||
};
|
||||
|
||||
const modinfoConsistent = (
|
||||
modinfo: NonNullable<KernelModule["modinfo"]>,
|
||||
): boolean => {
|
||||
const closed =
|
||||
modinfo.status === "malformed" || modinfo.status === "unsupported";
|
||||
if (closed) {
|
||||
if (modinfo.reason === null || modinfo.entries.length !== 0) return false;
|
||||
} else if (modinfo.reason !== null) return false;
|
||||
if (modinfo.status === "absent" || modinfo.status === "unknown") {
|
||||
if (
|
||||
modinfo.location !== null ||
|
||||
modinfo.sha256 !== null ||
|
||||
modinfo.entries.length !== 0
|
||||
)
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
modinfo.status === "present" &&
|
||||
(modinfo.location === null || modinfo.sha256 === null)
|
||||
)
|
||||
return false;
|
||||
return modinfo.entries.every((entry, index) => {
|
||||
if (entry.index !== index) return false;
|
||||
const rawFits = entry.location.bytes <= MAX_KERNEL_MODULE_RAW_BYTES;
|
||||
if (rawFits) {
|
||||
if (
|
||||
entry.raw_hex === null ||
|
||||
entry.raw_hex.length !== entry.location.bytes * 2
|
||||
)
|
||||
return false;
|
||||
} else if (entry.raw_hex !== null) return false;
|
||||
if (entry.status === "observed")
|
||||
return (
|
||||
entry.reason === null && entry.key !== null && entry.value !== null
|
||||
);
|
||||
return entry.reason !== null;
|
||||
});
|
||||
};
|
||||
|
||||
const versionsConsistent = (
|
||||
versions: NonNullable<KernelModule["versions"]>,
|
||||
): boolean => {
|
||||
if (
|
||||
versions.layout === null ||
|
||||
versions.entry_bytes !== KERNEL_MODULE_VERSION_ENTRY_BYTES
|
||||
)
|
||||
return false;
|
||||
const closed =
|
||||
versions.status === "malformed" || versions.status === "unsupported";
|
||||
if (closed) {
|
||||
if (versions.reason === null || versions.entries.length !== 0) return false;
|
||||
} else if (versions.reason !== null) return false;
|
||||
if (versions.status === "absent" || versions.status === "unknown") {
|
||||
if (
|
||||
versions.location !== null ||
|
||||
versions.sha256 !== null ||
|
||||
versions.entries.length !== 0
|
||||
)
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
versions.status === "present" &&
|
||||
(versions.location === null || versions.sha256 === null)
|
||||
)
|
||||
return false;
|
||||
const width = versions.layout === "elf64_unsigned_long_name56" ? 16 : 8;
|
||||
return versions.entries.every((entry, index) => {
|
||||
if (entry.index !== index || entry.crc_hex.length !== width) return false;
|
||||
if (entry.location.bytes !== KERNEL_MODULE_VERSION_ENTRY_BYTES)
|
||||
return false;
|
||||
if (entry.status === "observed")
|
||||
return entry.reason === null && entry.name !== null;
|
||||
return entry.reason !== null && entry.name === null;
|
||||
});
|
||||
};
|
||||
|
||||
const signatureConsistent = (
|
||||
signature: NonNullable<KernelModule["signature"]>,
|
||||
): boolean => {
|
||||
if (signature.status === "absent")
|
||||
return (
|
||||
signature.reason === null &&
|
||||
signature.magic === null &&
|
||||
signature.info === null &&
|
||||
signature.pkcs7 === null
|
||||
);
|
||||
if (signature.magic === null || signature.reason === null) {
|
||||
if (signature.status !== "present") return false;
|
||||
}
|
||||
if (signature.status === "present") {
|
||||
const info = signature.info;
|
||||
const pkcs7 = signature.pkcs7;
|
||||
if (
|
||||
signature.reason !== null ||
|
||||
signature.magic === null ||
|
||||
info === null ||
|
||||
pkcs7 === null
|
||||
)
|
||||
return false;
|
||||
return (
|
||||
info.id_type === MODULE_SIGNATURE_PKCS7_ID_TYPE &&
|
||||
info.algo === 0 &&
|
||||
info.hash === 0 &&
|
||||
info.signer_len === 0 &&
|
||||
info.key_id_len === 0 &&
|
||||
info.pad.every((value) => value === 0) &&
|
||||
info.sig_len === pkcs7.location.bytes &&
|
||||
pkcs7.location.offset + pkcs7.location.bytes === info.location.offset &&
|
||||
signature.magic.offset === info.location.offset + info.location.bytes &&
|
||||
signature.magic.bytes === MODULE_SIGNATURE_MAGIC.length &&
|
||||
info.location.bytes === MODULE_SIGNATURE_INFO_BYTES &&
|
||||
info.sig_len < info.location.offset
|
||||
);
|
||||
}
|
||||
if (signature.pkcs7 !== null || signature.reason === null) return false;
|
||||
if (signature.status === "unsupported")
|
||||
return (
|
||||
signature.reason === "signature_id_type_not_pkcs7" &&
|
||||
signature.info !== null &&
|
||||
signature.magic !== null
|
||||
);
|
||||
if (signature.reason === "signature_trailer_truncated")
|
||||
return signature.info === null && signature.magic !== null;
|
||||
return signature.info !== null && signature.magic !== null;
|
||||
};
|
||||
|
||||
const sizedConsistent = (
|
||||
section: NonNullable<KernelModule["init_text"]>,
|
||||
): boolean => {
|
||||
if (section.status === "absent" || section.status === "unknown")
|
||||
return (
|
||||
section.location === null &&
|
||||
section.bytes === null &&
|
||||
section.compressed === null
|
||||
);
|
||||
if (section.location === null) return section.bytes === null;
|
||||
return (
|
||||
section.bytes === section.location.bytes && section.compressed !== null
|
||||
);
|
||||
};
|
||||
|
||||
const metadataAbsent = (report: KernelModule): boolean =>
|
||||
report.modinfo === null &&
|
||||
report.versions === null &&
|
||||
report.this_module === null &&
|
||||
report.ksymtab === null &&
|
||||
report.init_text === null &&
|
||||
report.exit_text === null &&
|
||||
report.signature === null;
|
||||
|
||||
const validReport = (
|
||||
report: KernelModule,
|
||||
maxEntries: number,
|
||||
maxBytes: number,
|
||||
): boolean => {
|
||||
if (
|
||||
report.artifact.bytes > maxBytes ||
|
||||
report.coverage.examined_entries > maxEntries ||
|
||||
!rangesOf(report).every(inside(report.artifact.bytes))
|
||||
)
|
||||
return false;
|
||||
if (report.applicability === "unsupported")
|
||||
return (
|
||||
report.coverage.status === "unsupported" &&
|
||||
report.coverage.examined_entries === 0 &&
|
||||
report.compression !== null &&
|
||||
report.next_probe?.operation === "external_decompression" &&
|
||||
report.format === null &&
|
||||
report.byte_order === null &&
|
||||
report.machine === null &&
|
||||
report.type === null &&
|
||||
report.type_name === null &&
|
||||
metadataAbsent(report)
|
||||
);
|
||||
if (
|
||||
report.format === null ||
|
||||
report.byte_order === null ||
|
||||
report.machine === null ||
|
||||
report.type === null ||
|
||||
report.type_name !== elfObjectTypeName(report.type) ||
|
||||
report.compression !== null ||
|
||||
report.next_probe !== null
|
||||
)
|
||||
return false;
|
||||
if (report.applicability === "not_applicable")
|
||||
return (
|
||||
report.coverage.status === "not_applicable" &&
|
||||
report.coverage.examined_entries === 0 &&
|
||||
report.type !== 1 &&
|
||||
metadataAbsent(report)
|
||||
);
|
||||
if (report.applicability !== "applicable") return false;
|
||||
if (
|
||||
report.coverage.status !== "complete" ||
|
||||
report.modinfo === null ||
|
||||
report.versions === null ||
|
||||
report.this_module === null ||
|
||||
report.this_module.layout !== "unknown" ||
|
||||
report.ksymtab === null ||
|
||||
report.init_text === null ||
|
||||
report.exit_text === null ||
|
||||
report.signature === null
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
report.coverage.examined_entries !==
|
||||
report.modinfo.entries.length +
|
||||
report.versions.entries.length +
|
||||
report.ksymtab.sections.length
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
!report.ksymtab.sections.every(
|
||||
(section) => (section.name === null) === (section.reason !== null),
|
||||
)
|
||||
)
|
||||
return false;
|
||||
if (report.type !== 1) return false;
|
||||
return (
|
||||
modinfoConsistent(report.modinfo) &&
|
||||
versionsConsistent(report.versions) &&
|
||||
signatureConsistent(report.signature) &&
|
||||
sizedConsistent(report.init_text) &&
|
||||
sizedConsistent(report.exit_text)
|
||||
);
|
||||
};
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
createBinaryLayoutService,
|
||||
createElfNotesService,
|
||||
createAppleIpsService,
|
||||
createKernelModuleService,
|
||||
createPeLoadConfigService,
|
||||
createPeResourcesService,
|
||||
createPeCertificatesService,
|
||||
@@ -26,6 +27,7 @@ import { inspectPeImportsInputSchema } from "../domain/native/peImports.js";
|
||||
import { inspectNativeExportsInputSchema } from "../domain/native/nativeExports.js";
|
||||
import { inspectElfNotesInputSchema } from "../domain/native/elfNotes.js";
|
||||
import { inspectAppleIpsInputSchema } from "../domain/native/appleIps.js";
|
||||
import { inspectKernelModuleInputSchema } from "../domain/native/kernelModule.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
|
||||
import { localPathStringSchema } from "../domain/localPath.js";
|
||||
@@ -120,6 +122,43 @@ export const registerBinaryDiagnosticsCommands = (
|
||||
);
|
||||
}),
|
||||
});
|
||||
cli.command(CLI_COMMANDS.inspectKernelModule, {
|
||||
description:
|
||||
"Inspect Linux kernel module metadata without executing the module",
|
||||
args: z.object({
|
||||
path: localPathStringSchema.describe(
|
||||
"Local ELF32/ELF64 kernel module or compressed .ko container",
|
||||
),
|
||||
}),
|
||||
options: z.object({
|
||||
maxFileBytes: inspectKernelModuleInputSchema.shape.max_file_bytes,
|
||||
maxEntries: inspectKernelModuleInputSchema.shape.max_entries,
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
withCommandCancellation(async (signal) => {
|
||||
const kernelModule = createKernelModuleService();
|
||||
return await logCliCommand(
|
||||
logger,
|
||||
CLI_COMMANDS.inspectKernelModule,
|
||||
async () =>
|
||||
finishAnalysisCommand(
|
||||
() =>
|
||||
kernelModule.inspect(
|
||||
{
|
||||
path: absoluteCallerPath(args.path),
|
||||
max_file_bytes: options.maxFileBytes,
|
||||
max_entries: options.maxEntries,
|
||||
},
|
||||
{ signal },
|
||||
),
|
||||
kernelModule,
|
||||
"rea-kernel-module",
|
||||
"inspect_kernel_module",
|
||||
signal,
|
||||
),
|
||||
);
|
||||
}),
|
||||
});
|
||||
cli.command(CLI_COMMANDS.inspectPeLoadConfig, {
|
||||
description:
|
||||
"Inspect PE TLS callbacks and load configuration without executing the image",
|
||||
|
||||
@@ -75,6 +75,7 @@ export const CLI_COMMANDS = Object.freeze({
|
||||
inspectNativeExports: "inspect-native-exports",
|
||||
inspectElfNotes: "inspect-elf-notes",
|
||||
inspectAppleIps: "inspect-apple-ips",
|
||||
inspectKernelModule: "inspect-kernel-module",
|
||||
inspectPeLoadConfig: "inspect-pe-load-config",
|
||||
inspectAnalysisView: "inspect-analysis-view",
|
||||
listFridaDevices: "list-frida-devices",
|
||||
|
||||
@@ -16,6 +16,8 @@ import { ElfNotesService } from "../application/binaryDiagnostics/ElfNotesServic
|
||||
import { ElfNotesProvider } from "../native/elf/ElfNotesProvider.js";
|
||||
import { AppleIpsService } from "../application/binaryDiagnostics/AppleIpsService.js";
|
||||
import { AppleIpsProvider } from "../native/apple/AppleIpsProvider.js";
|
||||
import { KernelModuleService } from "../application/binaryDiagnostics/KernelModuleService.js";
|
||||
import { KernelModuleProvider } from "../native/elf/KernelModuleProvider.js";
|
||||
import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
|
||||
import { NativeExportsService } from "../application/binaryDiagnostics/NativeExportsService.js";
|
||||
import { NativeExportsProvider } from "../native/exports/NativeExportsProvider.js";
|
||||
@@ -44,6 +46,9 @@ export const createElfNotesService = (): ElfNotesService =>
|
||||
/** Portable Apple .ips inspection with no engine, Xcode or target acquisition. */
|
||||
export const createAppleIpsService = (): AppleIpsService =>
|
||||
new AppleIpsService(new AppleIpsProvider());
|
||||
/** Portable Linux kernel-module metadata inspection with no engine or host `modinfo`. */
|
||||
export const createKernelModuleService = (): KernelModuleService =>
|
||||
new KernelModuleService(new KernelModuleProvider());
|
||||
|
||||
/** Compose the offline adapter without import-time I/O or dependency acquisition. */
|
||||
export const createBinaryLayoutService = (
|
||||
|
||||
@@ -48,6 +48,10 @@ import {
|
||||
inspectAppleIpsInputSchema,
|
||||
appleIpsSchema,
|
||||
} from "../../domain/native/appleIps.js";
|
||||
import {
|
||||
inspectKernelModuleInputSchema,
|
||||
kernelModuleSchema,
|
||||
} from "../../domain/native/kernelModule.js";
|
||||
|
||||
/** Caller-selected delivery of completed binary layout Evidence. */
|
||||
export const inspectBinaryLayoutRequestSchema =
|
||||
@@ -112,6 +116,22 @@ export const BINARY_DIAGNOSTICS_TOOL_CONTRACTS = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "inspect_kernel_module",
|
||||
...toolContractMetadata("inspect_kernel_module"),
|
||||
kind: "native-provider",
|
||||
description:
|
||||
"Inspect one explicit local ELF32/ELF64 ET_REL Linux kernel module without executing it or opening a disassembler. Returns ordered .modinfo key/value entries (duplicates preserved), __versions symbol CRCs, presence of .gnu.linkonce.this_module and __ksymtab sections, observed .init.text and .exit.text sizes, and an appended module signature trailer with its module_signature fields plus a PKCS#7 byte range and SHA-256. Does not execute the module, decode struct module, or validate certificates. A missing section is observed absence when section names are known, not proof the module never carried that metadata. Other ELF types are not applicable. gzip, xz, and zstd containers are unsupported and name external_decompression as the next probe; neither this tool nor extract_artifact decodes those codecs, so decompress with an external utility and inspect the result. Reads a stable regular file; malformed ELF headers and exhausted entry budgets return no partial success. Portable on supported Node hosts.",
|
||||
inputSchema: inspectKernelModuleInputSchema,
|
||||
outputSchema: evidenceResultOf(kernelModuleSchema),
|
||||
examples: [
|
||||
{
|
||||
title:
|
||||
"Inspect Linux kernel module license, vermagic, and dependencies",
|
||||
input: { path: "/artifacts/demo.ko" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "inspect_byte_entropy",
|
||||
...toolContractMetadata("inspect_byte_entropy"),
|
||||
|
||||
@@ -30,6 +30,7 @@ const EXECUTION_WORDING_EXCEPTIONS: Readonly<Record<string, string>> = {
|
||||
inspect_native_exports: "reads the export table without executing it",
|
||||
inspect_elf_notes: "states that it does not execute the image",
|
||||
inspect_apple_ips: "states that it does not execute a target",
|
||||
inspect_kernel_module: "states that it does not execute the module",
|
||||
inspect_pe_load_config: "states that it does not execute the image",
|
||||
inspect_desktop_application:
|
||||
"states that directory triage performs no target execution",
|
||||
|
||||
@@ -262,6 +262,7 @@ export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
|
||||
inspect_native_exports: evidence,
|
||||
inspect_elf_notes: evidence,
|
||||
inspect_apple_ips: evidence,
|
||||
inspect_kernel_module: evidence,
|
||||
inspect_pe_load_config: evidence,
|
||||
inspect_analysis_view: evidence,
|
||||
inspect_firmware_regions: effects({
|
||||
|
||||
@@ -6,6 +6,7 @@ import { z } from "zod";
|
||||
import { packetFieldsSchema } from "../domain/captures/packetCapture.js";
|
||||
import { swiftStringLiteralSchema } from "../domain/hopperValues.js";
|
||||
import { applicationVersionComparisonResultSchema } from "../domain/javascript/javascriptApplicationVersionComparisonSchemas.js";
|
||||
import { kernelModuleSchema } from "../domain/native/kernelModule.js";
|
||||
import { pinvokeVerificationSchema } from "../domain/managed/managedNativeVerificationSchemas.js";
|
||||
import { relationshipCoverageSchema } from "../domain/relationshipCoverage.js";
|
||||
import { compactAdvertisedInputSchema } from "./compactInputSchema.js";
|
||||
@@ -63,6 +64,17 @@ const nativeSymbol = {
|
||||
source: "function-dossier",
|
||||
};
|
||||
|
||||
const signatureInfo = {
|
||||
location: { offset: 16, bytes: 12 },
|
||||
algo: 0,
|
||||
hash: 0,
|
||||
id_type: 2,
|
||||
signer_len: 0,
|
||||
key_id_len: 0,
|
||||
pad: [0, 0, 0],
|
||||
sig_len: 16,
|
||||
};
|
||||
|
||||
const cases: readonly {
|
||||
readonly name: string;
|
||||
readonly schema: z.ZodType;
|
||||
@@ -112,6 +124,16 @@ const cases: readonly {
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "kernel module signature pad bytes",
|
||||
schema: kernelModuleSchema.shape.signature.unwrap().shape.info.unwrap(),
|
||||
samples: [
|
||||
{ value: signatureInfo, valid: true },
|
||||
...[[], [0], [0, 0, 0, 0], [0, null, 0], [0, 256, 0], null].map(
|
||||
(pad) => ({ value: { ...signatureInfo, pad }, valid: false }),
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "packet frame numbers",
|
||||
schema: packetFieldsSchema,
|
||||
|
||||
@@ -32,6 +32,7 @@ export class ArtifactOperationError extends AnalysisError {
|
||||
| "inspect_pe_resources"
|
||||
| "inspect_pe_certificates"
|
||||
| "inspect_elf_notes"
|
||||
| "inspect_kernel_module"
|
||||
| "inspect_erlang_beam"
|
||||
| "extract_artifact"
|
||||
| "decode_interface_builder"
|
||||
|
||||
@@ -53,6 +53,12 @@ export const APPLE_IPS_PROVIDER = Object.freeze({
|
||||
name: "REA Apple IPS parser",
|
||||
version: "1",
|
||||
} as const);
|
||||
/** Provider identity for portable, execution-free Linux kernel-module metadata. */
|
||||
export const KERNEL_MODULE_PROVIDER = Object.freeze({
|
||||
id: "rea-kernel-module",
|
||||
name: "REA kernel module parser",
|
||||
version: "1",
|
||||
} as const);
|
||||
|
||||
/** Provider identity for execution-free Shannon byte-entropy profiling. */
|
||||
export const BYTE_ENTROPY_PROVIDER = Object.freeze({
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { inspectKernelModuleInputSchema } from "./kernelModule.js";
|
||||
|
||||
it("rejects extra caller arguments and applies kernel-module budgets", () => {
|
||||
expect(
|
||||
inspectKernelModuleInputSchema.safeParse({
|
||||
path: "/artifacts/demo.ko",
|
||||
extra: true,
|
||||
}).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
inspectKernelModuleInputSchema.parse({ path: "/artifacts/demo.ko" }),
|
||||
).toMatchObject({
|
||||
max_file_bytes: 67108864,
|
||||
max_entries: 8192,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,270 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { digestSchema } from "../digests.js";
|
||||
import { fixedTupleSchema } from "../fixedTupleSchema.js";
|
||||
import { localPathStringSchema } from "../localPath.js";
|
||||
import { ELF_OBJECT_TYPE_NAMES } from "./elfNotes.js";
|
||||
|
||||
const uint = z.number().int().min(0).max(0xffffffff);
|
||||
const uint8 = z.number().int().min(0).max(255);
|
||||
const range = z.strictObject({ offset: uint, bytes: uint });
|
||||
const hex = z.string().regex(/^[0-9a-f]*$/u);
|
||||
|
||||
/** Hard ceiling on the caller-selected input read budget. */
|
||||
export const MAX_INSPECT_KERNEL_MODULE_FILE_BYTES = 536870912;
|
||||
/**
|
||||
* Hard ceiling on decoded .modinfo and __versions entries plus listed
|
||||
* file-backed __ksymtab* sections combined.
|
||||
*/
|
||||
export const MAX_INSPECT_KERNEL_MODULE_ENTRIES = 65536;
|
||||
/**
|
||||
* Retained .modinfo key length. Longer keys stay malformed per entry and are
|
||||
* not copied into the result.
|
||||
*/
|
||||
export const MAX_KERNEL_MODULE_KEY_BYTES = 256;
|
||||
/**
|
||||
* Retained .modinfo value length. Longer values stay malformed per entry and
|
||||
* are not copied into the result.
|
||||
*/
|
||||
export const MAX_KERNEL_MODULE_VALUE_BYTES = 4096;
|
||||
/**
|
||||
* Retained __ksymtab* section-name length. Longer names stay listed with a
|
||||
* null name and are not copied into the result.
|
||||
*/
|
||||
export const MAX_KERNEL_MODULE_SECTION_NAME_BYTES = 256;
|
||||
/** Inline raw-entry hex ceiling. Longer entries keep only their SHA-256. */
|
||||
export const MAX_KERNEL_MODULE_RAW_BYTES = 4096;
|
||||
/**
|
||||
* `struct modversion_info` is `unsigned long crc` plus
|
||||
* `char name[64 - sizeof(unsigned long)]`, so every entry is 64 bytes.
|
||||
* ELF32 stores a u32 CRC and a 60-byte name. ELF64 stores a u64
|
||||
* unsigned long and a 56-byte name.
|
||||
*/
|
||||
export const KERNEL_MODULE_VERSION_ENTRY_BYTES = 64;
|
||||
/** `struct module_signature` is 12 bytes, with sig_len big-endian. */
|
||||
export const MODULE_SIGNATURE_INFO_BYTES = 12;
|
||||
/** Trailer magic, including the trailing newline, from MODULE_SIG_STRING. */
|
||||
export const MODULE_SIGNATURE_MAGIC = "~Module signature appended~\n";
|
||||
/** `PKEY_ID_PKCS7` from the kernel public-key identifier enum. */
|
||||
export const MODULE_SIGNATURE_PKCS7_ID_TYPE = 2;
|
||||
|
||||
export const KERNEL_MODULE_APPLICABILITY = [
|
||||
"applicable",
|
||||
"not_applicable",
|
||||
"unsupported",
|
||||
] as const;
|
||||
|
||||
export const KERNEL_MODULE_MODINFO_ENTRY_REASONS = [
|
||||
"unterminated_modinfo_entry",
|
||||
"modinfo_missing_separator",
|
||||
"modinfo_empty_key",
|
||||
"modinfo_field_exceeds_limit",
|
||||
] as const;
|
||||
|
||||
export const KERNEL_MODULE_MODINFO_SECTION_REASONS = [
|
||||
"modinfo_section_compressed",
|
||||
"modinfo_section_not_file_backed",
|
||||
] as const;
|
||||
|
||||
export const KERNEL_MODULE_VERSION_ENTRY_REASONS = [
|
||||
"version_name_unterminated",
|
||||
] as const;
|
||||
|
||||
export const KERNEL_MODULE_VERSION_SECTION_REASONS = [
|
||||
"versions_size_not_multiple",
|
||||
"versions_section_compressed",
|
||||
"versions_section_not_file_backed",
|
||||
] as const;
|
||||
|
||||
export const KERNEL_MODULE_KSYMTAB_SECTION_REASONS = [
|
||||
"ksymtab_name_exceeds_limit",
|
||||
] as const;
|
||||
|
||||
export const KERNEL_MODULE_SIGNATURE_REASONS = [
|
||||
"signature_trailer_truncated",
|
||||
"signature_length_inconsistent",
|
||||
"signature_parameters_nonzero",
|
||||
"signature_id_type_not_pkcs7",
|
||||
] as const;
|
||||
|
||||
export const KERNEL_MODULE_VERSION_LAYOUTS = [
|
||||
"elf32_unsigned_long_name60",
|
||||
"elf64_unsigned_long_name56",
|
||||
] as const;
|
||||
|
||||
const sectionStatus = z.enum([
|
||||
"present",
|
||||
"absent",
|
||||
"unknown",
|
||||
"malformed",
|
||||
"unsupported",
|
||||
]);
|
||||
|
||||
const entryStatus = z.enum(["observed", "malformed"]);
|
||||
|
||||
const modinfoEntrySchema = z.strictObject({
|
||||
index: uint,
|
||||
location: range,
|
||||
key: z.string().nullable(),
|
||||
value: z.string().nullable(),
|
||||
raw_sha256: digestSchema,
|
||||
raw_hex: hex.nullable(),
|
||||
status: entryStatus,
|
||||
reason: z.enum(KERNEL_MODULE_MODINFO_ENTRY_REASONS).nullable(),
|
||||
});
|
||||
|
||||
const modinfoSchema = z.strictObject({
|
||||
status: sectionStatus,
|
||||
location: range.nullable(),
|
||||
sha256: digestSchema.nullable(),
|
||||
entries: z.array(modinfoEntrySchema),
|
||||
reason: z.enum(KERNEL_MODULE_MODINFO_SECTION_REASONS).nullable(),
|
||||
});
|
||||
|
||||
const versionEntrySchema = z.strictObject({
|
||||
index: uint,
|
||||
location: range,
|
||||
crc_hex: z.string().regex(/^(?:[0-9a-f]{8}|[0-9a-f]{16})$/u),
|
||||
name: z.string().nullable(),
|
||||
status: entryStatus,
|
||||
reason: z.enum(KERNEL_MODULE_VERSION_ENTRY_REASONS).nullable(),
|
||||
});
|
||||
|
||||
const versionsSchema = z.strictObject({
|
||||
status: sectionStatus,
|
||||
layout: z.enum(KERNEL_MODULE_VERSION_LAYOUTS).nullable(),
|
||||
entry_bytes: z.literal(KERNEL_MODULE_VERSION_ENTRY_BYTES).nullable(),
|
||||
location: range.nullable(),
|
||||
sha256: digestSchema.nullable(),
|
||||
entries: z.array(versionEntrySchema),
|
||||
reason: z.enum(KERNEL_MODULE_VERSION_SECTION_REASONS).nullable(),
|
||||
});
|
||||
|
||||
const thisModuleSchema = z.strictObject({
|
||||
status: z.enum(["present", "absent", "unknown", "unsupported"]),
|
||||
location: range.nullable(),
|
||||
/** `struct module` changes with the kernel version and is not decoded. */
|
||||
layout: z.literal("unknown"),
|
||||
});
|
||||
|
||||
const sizedSectionSchema = z.strictObject({
|
||||
status: z.enum(["present", "absent", "unknown"]),
|
||||
location: range.nullable(),
|
||||
/** Observed file-backed size. Null when the section is not file-backed. */
|
||||
bytes: uint.nullable(),
|
||||
compressed: z.boolean().nullable(),
|
||||
});
|
||||
|
||||
const ksymtabSchema = z.strictObject({
|
||||
status: z.enum(["observed", "unknown"]),
|
||||
sections: z.array(
|
||||
z.strictObject({
|
||||
name: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(MAX_KERNEL_MODULE_SECTION_NAME_BYTES)
|
||||
.nullable(),
|
||||
location: range,
|
||||
compressed: z.boolean(),
|
||||
reason: z.enum(KERNEL_MODULE_KSYMTAB_SECTION_REASONS).nullable(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const signatureInfoSchema = z.strictObject({
|
||||
location: range,
|
||||
algo: uint8,
|
||||
hash: uint8,
|
||||
id_type: uint8,
|
||||
signer_len: uint8,
|
||||
key_id_len: uint8,
|
||||
pad: fixedTupleSchema([uint8, uint8, uint8]),
|
||||
sig_len: uint,
|
||||
});
|
||||
|
||||
const signatureSchema = z.strictObject({
|
||||
status: z.enum(["absent", "present", "malformed", "unsupported"]),
|
||||
magic: range.nullable(),
|
||||
info: signatureInfoSchema.nullable(),
|
||||
pkcs7: z
|
||||
.strictObject({
|
||||
location: range,
|
||||
sha256: digestSchema,
|
||||
})
|
||||
.nullable(),
|
||||
reason: z.enum(KERNEL_MODULE_SIGNATURE_REASONS).nullable(),
|
||||
});
|
||||
|
||||
/** Complete bounded inspection of one relocatable Linux kernel module. */
|
||||
export const inspectKernelModuleInputSchema = z.strictObject({
|
||||
path: localPathStringSchema.describe(
|
||||
"Absolute filesystem path to an ELF kernel module or a compressed .ko container",
|
||||
),
|
||||
max_file_bytes: z
|
||||
.number()
|
||||
.int()
|
||||
.min(64)
|
||||
.max(MAX_INSPECT_KERNEL_MODULE_FILE_BYTES)
|
||||
.default(67108864)
|
||||
.describe("Maximum stable artifact bytes to read (default 64 MiB)"),
|
||||
max_entries: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(MAX_INSPECT_KERNEL_MODULE_ENTRIES)
|
||||
.default(8192)
|
||||
.describe(
|
||||
"Maximum .modinfo and __versions entries plus file-backed __ksymtab* sections to examine; exceeding the budget fails without partial success",
|
||||
),
|
||||
});
|
||||
|
||||
/**
|
||||
* Observed kernel-module metadata. ET_REL images are applicable. Other ELF
|
||||
* types are not applicable. gzip, xz, and zstd containers are unsupported.
|
||||
*/
|
||||
export const kernelModuleSchema = z.strictObject({
|
||||
artifact: z.strictObject({
|
||||
path: z.string().min(1),
|
||||
sha256: digestSchema,
|
||||
bytes: uint,
|
||||
}),
|
||||
format: z.enum(["elf32", "elf64"]).nullable(),
|
||||
byte_order: z.enum(["little", "big"]).nullable(),
|
||||
machine: uint.nullable(),
|
||||
type: uint.nullable(),
|
||||
type_name: z.enum(ELF_OBJECT_TYPE_NAMES).nullable(),
|
||||
applicability: z.enum(KERNEL_MODULE_APPLICABILITY),
|
||||
compression: z
|
||||
.strictObject({
|
||||
codec: z.enum(["gzip", "xz", "zstd"]),
|
||||
status: z.literal("unsupported"),
|
||||
})
|
||||
.nullable(),
|
||||
next_probe: z
|
||||
.strictObject({
|
||||
operation: z
|
||||
.literal("external_decompression")
|
||||
.describe(
|
||||
"External decompression with a codec utility, not a REA tool",
|
||||
),
|
||||
rationale: z.string().min(1),
|
||||
})
|
||||
.nullable(),
|
||||
modinfo: modinfoSchema.nullable(),
|
||||
versions: versionsSchema.nullable(),
|
||||
this_module: thisModuleSchema.nullable(),
|
||||
ksymtab: ksymtabSchema.nullable(),
|
||||
init_text: sizedSectionSchema.nullable(),
|
||||
exit_text: sizedSectionSchema.nullable(),
|
||||
signature: signatureSchema.nullable(),
|
||||
coverage: z.strictObject({
|
||||
status: z.enum(["complete", "not_applicable", "unsupported"]),
|
||||
examined_entries: uint,
|
||||
}),
|
||||
limitations: z.array(z.string()),
|
||||
});
|
||||
|
||||
export type KernelModule = z.infer<typeof kernelModuleSchema>;
|
||||
export type InspectKernelModuleInput = z.infer<
|
||||
typeof inspectKernelModuleInputSchema
|
||||
>;
|
||||
@@ -0,0 +1,254 @@
|
||||
import { MODULE_SIGNATURE_MAGIC } from "../../domain/native/kernelModule.js";
|
||||
|
||||
const concat = (...parts: readonly Buffer[]): Buffer => Buffer.concat(parts);
|
||||
|
||||
const write16 = (
|
||||
bytes: Buffer,
|
||||
offset: number,
|
||||
value: number,
|
||||
little: boolean,
|
||||
): void => {
|
||||
if (little) bytes.writeUInt16LE(value, offset);
|
||||
else bytes.writeUInt16BE(value, offset);
|
||||
};
|
||||
|
||||
const write32 = (
|
||||
bytes: Buffer,
|
||||
offset: number,
|
||||
value: number,
|
||||
little: boolean,
|
||||
): void => {
|
||||
if (little) bytes.writeUInt32LE(value, offset);
|
||||
else bytes.writeUInt32BE(value, offset);
|
||||
};
|
||||
|
||||
const writeWord = (
|
||||
bytes: Buffer,
|
||||
offset: number,
|
||||
value: number,
|
||||
bits: 32 | 64,
|
||||
little: boolean,
|
||||
): void => {
|
||||
if (bits === 64) {
|
||||
const wide = BigInt(value);
|
||||
if (little) bytes.writeBigUInt64LE(wide, offset);
|
||||
else bytes.writeBigUInt64BE(wide, offset);
|
||||
return;
|
||||
}
|
||||
write32(bytes, offset, value, little);
|
||||
};
|
||||
|
||||
/** One `__versions` record: unsigned long CRC plus a fixed NUL-padded name. */
|
||||
export const modversionEntry = (
|
||||
crc: number | bigint,
|
||||
name: string,
|
||||
bits: 32 | 64,
|
||||
little: boolean,
|
||||
): Buffer => {
|
||||
const bytes = Buffer.alloc(64);
|
||||
const encoded = Buffer.from(name, "latin1");
|
||||
if (bits === 64) {
|
||||
const value = BigInt(crc);
|
||||
if (little) bytes.writeBigUInt64LE(value, 0);
|
||||
else bytes.writeBigUInt64BE(value, 0);
|
||||
if (encoded.length >= 56)
|
||||
throw new Error("ELF64 modversion name must fit in 56 bytes");
|
||||
encoded.copy(bytes, 8);
|
||||
return bytes;
|
||||
}
|
||||
if (little) bytes.writeUInt32LE(Number(crc), 0);
|
||||
else bytes.writeUInt32BE(Number(crc), 0);
|
||||
if (encoded.length >= 60)
|
||||
throw new Error("ELF32 modversion name must fit in 60 bytes");
|
||||
encoded.copy(bytes, 4);
|
||||
return bytes;
|
||||
};
|
||||
|
||||
/**
|
||||
* Appended module signature trailer. `sigLen` defaults to the PKCS#7 length;
|
||||
* a larger value builds an inconsistent trailer.
|
||||
*/
|
||||
export const moduleSignatureTrailer = (
|
||||
pkcs7: Buffer,
|
||||
sigLen = pkcs7.length,
|
||||
): Buffer => {
|
||||
const info = Buffer.alloc(12);
|
||||
info[2] = 2;
|
||||
info.writeUInt32BE(sigLen >>> 0, 8);
|
||||
return concat(pkcs7, info, Buffer.from(MODULE_SIGNATURE_MAGIC, "latin1"));
|
||||
};
|
||||
|
||||
export interface KernelModuleFixtureOptions {
|
||||
readonly bits?: 32 | 64;
|
||||
readonly little?: boolean;
|
||||
readonly type?: number;
|
||||
readonly machine?: number;
|
||||
/** Null omits the section. Undefined uses the synthetic default. */
|
||||
readonly modinfo?: Buffer | null;
|
||||
readonly versions?: Buffer | null;
|
||||
readonly thisModule?: Buffer | null;
|
||||
readonly ksymtab?: Buffer | null;
|
||||
readonly initText?: Buffer | null;
|
||||
readonly exitText?: Buffer | null;
|
||||
/** Null omits the trailer. Undefined appends a consistent PKCS#7 trailer. */
|
||||
readonly signature?: Buffer | null;
|
||||
/** Declare a second `.modinfo` section with the same bytes. */
|
||||
readonly repeatModinfo?: boolean;
|
||||
/** Extra empty file-backed sections that share one name-table string. */
|
||||
readonly sharedNameSections?: {
|
||||
readonly name: string;
|
||||
readonly count: number;
|
||||
};
|
||||
}
|
||||
|
||||
const defaultModinfo = (): Buffer =>
|
||||
Buffer.from(
|
||||
"license=GPL\0vermagic=5.15.0 SMP mod_unload\0alias=pci:v1\0alias=pci:v2\0name=demo\0depends=\0",
|
||||
"latin1",
|
||||
);
|
||||
|
||||
const defaultVersions = (bits: 32 | 64, little: boolean): Buffer =>
|
||||
concat(
|
||||
modversionEntry(0x12345678, "printk", bits, little),
|
||||
modversionEntry(0x90abcdef, "module_layout", bits, little),
|
||||
);
|
||||
|
||||
const defaultPkcs7 = (): Buffer => Buffer.from("fake-pkcs7-bytes", "latin1");
|
||||
|
||||
interface SectionBody {
|
||||
readonly name: string;
|
||||
readonly type: number;
|
||||
readonly body: Buffer;
|
||||
}
|
||||
|
||||
/** Synthetic ET_REL image with kernel-module sections and an optional trailer. */
|
||||
export const kernelModuleFixture = (
|
||||
options: KernelModuleFixtureOptions = {},
|
||||
): Buffer => {
|
||||
const bits = options.bits ?? 64;
|
||||
const little = options.little ?? true;
|
||||
const chosen = (
|
||||
value: Buffer | null | undefined,
|
||||
fallback: () => Buffer,
|
||||
): Buffer | null =>
|
||||
value === null ? null : value === undefined ? fallback() : value;
|
||||
const bodies: SectionBody[] = [];
|
||||
const add = (name: string, body: Buffer | null): void => {
|
||||
if (body !== null) bodies.push({ name, type: 1, body });
|
||||
};
|
||||
add(".modinfo", chosen(options.modinfo, defaultModinfo));
|
||||
if (options.repeatModinfo === true) {
|
||||
const first = bodies.find((section) => section.name === ".modinfo");
|
||||
if (first !== undefined)
|
||||
bodies.push({ ...first, body: Buffer.from(first.body) });
|
||||
}
|
||||
add(
|
||||
"__versions",
|
||||
chosen(options.versions, () => defaultVersions(bits, little)),
|
||||
);
|
||||
add(
|
||||
".gnu.linkonce.this_module",
|
||||
chosen(options.thisModule, () => Buffer.alloc(8, 0x11)),
|
||||
);
|
||||
add(
|
||||
"__ksymtab",
|
||||
chosen(options.ksymtab, () => Buffer.alloc(8, 0x22)),
|
||||
);
|
||||
add(
|
||||
".init.text",
|
||||
chosen(options.initText, () => Buffer.from([0x90, 0x90, 0x90, 0x90])),
|
||||
);
|
||||
add(
|
||||
".exit.text",
|
||||
chosen(options.exitText, () => Buffer.from([0xc3])),
|
||||
);
|
||||
const shared = options.sharedNameSections;
|
||||
if (shared !== undefined)
|
||||
for (let index = 0; index < shared.count; index++)
|
||||
bodies.push({ name: shared.name, type: 1, body: Buffer.alloc(0) });
|
||||
const labels = [
|
||||
...new Set([...bodies.map((section) => section.name), ".shstrtab"]),
|
||||
];
|
||||
const nameOffsets = new Map<string, number>();
|
||||
const nameParts: Buffer[] = [Buffer.from([0])];
|
||||
for (const label of labels) {
|
||||
nameOffsets.set(
|
||||
label,
|
||||
nameParts.reduce((sum, part) => sum + part.length, 0),
|
||||
);
|
||||
nameParts.push(Buffer.from(`${label}\0`, "latin1"));
|
||||
}
|
||||
const names = concat(...nameParts);
|
||||
const headerSize = bits === 64 ? 64 : 52;
|
||||
const shentsize = bits === 64 ? 64 : 40;
|
||||
const shnum = bodies.length + 2;
|
||||
const shoff = headerSize;
|
||||
let cursor = shoff + shnum * shentsize;
|
||||
const placed = bodies.map((section) => {
|
||||
const offset = cursor;
|
||||
cursor += section.body.length;
|
||||
return { ...section, offset };
|
||||
});
|
||||
const namesOffset = cursor;
|
||||
cursor += names.length;
|
||||
const signature =
|
||||
options.signature === null
|
||||
? Buffer.alloc(0)
|
||||
: (options.signature ?? moduleSignatureTrailer(defaultPkcs7()));
|
||||
const bytes = Buffer.alloc(cursor + signature.length);
|
||||
bytes.writeUInt32LE(0x464c457f, 0);
|
||||
bytes[4] = bits === 64 ? 2 : 1;
|
||||
bytes[5] = little ? 1 : 2;
|
||||
bytes[6] = 1;
|
||||
write16(bytes, 16, options.type ?? 1, little);
|
||||
write16(bytes, 18, options.machine ?? (bits === 64 ? 62 : 20), little);
|
||||
write32(bytes, 20, 1, little);
|
||||
write16(bytes, bits === 64 ? 52 : 40, headerSize, little);
|
||||
write16(bytes, bits === 64 ? 54 : 42, bits === 64 ? 56 : 32, little);
|
||||
writeWord(bytes, bits === 64 ? 40 : 32, shoff, bits, little);
|
||||
write16(bytes, bits === 64 ? 58 : 46, shentsize, little);
|
||||
write16(bytes, bits === 64 ? 60 : 48, shnum, little);
|
||||
write16(bytes, bits === 64 ? 62 : 50, shnum - 1, little);
|
||||
const writeSection = (
|
||||
index: number,
|
||||
name: string,
|
||||
type: number,
|
||||
offset: number,
|
||||
size: number,
|
||||
): void => {
|
||||
const at = shoff + index * shentsize;
|
||||
write32(bytes, at, nameOffsets.get(name) ?? 0, little);
|
||||
write32(bytes, at + 4, type, little);
|
||||
writeWord(bytes, at + (bits === 64 ? 24 : 16), offset, bits, little);
|
||||
writeWord(bytes, at + (bits === 64 ? 32 : 20), size, bits, little);
|
||||
writeWord(bytes, at + (bits === 64 ? 48 : 32), 1, bits, little);
|
||||
};
|
||||
placed.forEach((section, index) => {
|
||||
writeSection(
|
||||
index + 1,
|
||||
section.name,
|
||||
section.type,
|
||||
section.offset,
|
||||
section.body.length,
|
||||
);
|
||||
section.body.copy(bytes, section.offset);
|
||||
});
|
||||
writeSection(shnum - 1, ".shstrtab", 3, namesOffset, names.length);
|
||||
names.copy(bytes, namesOffset);
|
||||
signature.copy(bytes, cursor);
|
||||
return bytes;
|
||||
};
|
||||
|
||||
/** Whole-file compression magics this operation recognizes and does not decode. */
|
||||
export const compressedKernelModuleContainer = (
|
||||
codec: "gzip" | "xz" | "zstd",
|
||||
): Buffer => {
|
||||
switch (codec) {
|
||||
case "gzip":
|
||||
return Buffer.from([0x1f, 0x8b, 0x08, 0x00]);
|
||||
case "xz":
|
||||
return Buffer.from([0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00, 0x00]);
|
||||
case "zstd":
|
||||
return Buffer.from([0x28, 0xb5, 0x2f, 0xfd, 0x00]);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,451 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { expect, it } from "vitest";
|
||||
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
|
||||
import {
|
||||
inspectKernelModuleInputSchema,
|
||||
kernelModuleSchema,
|
||||
MODULE_SIGNATURE_MAGIC,
|
||||
} from "../../domain/native/kernelModule.js";
|
||||
import { BinaryFormatFailure } from "../metadata/BinaryContainer.js";
|
||||
import {
|
||||
compressedKernelModuleContainer,
|
||||
kernelModuleFixture,
|
||||
modversionEntry,
|
||||
moduleSignatureTrailer,
|
||||
} from "./KernelModule.fixture.js";
|
||||
import { parseKernelModule } from "./KernelModuleParser.js";
|
||||
|
||||
const artifactPath = "/artifacts/demo.ko";
|
||||
|
||||
const parse = (bytes: Buffer, extra: { readonly max_entries?: number } = {}) =>
|
||||
kernelModuleSchema.parse(
|
||||
parseKernelModule(
|
||||
bytes,
|
||||
{
|
||||
path: artifactPath,
|
||||
sha256: "ab".repeat(32),
|
||||
bytes: bytes.length,
|
||||
},
|
||||
inspectKernelModuleInputSchema.parse({
|
||||
path: artifactPath,
|
||||
...extra,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const pkcs7 = Buffer.from("fake-pkcs7-bytes", "latin1");
|
||||
const pkcs7Sha256 = createHash("sha256").update(pkcs7).digest("hex");
|
||||
|
||||
it("preserves parameter duplicates, embedded equals, and original entry bytes", () => {
|
||||
const records = [
|
||||
"parm=level:verbosity=a",
|
||||
"parmtype=level:int",
|
||||
"parm=level:again",
|
||||
];
|
||||
const report = parse(
|
||||
kernelModuleFixture({ modinfo: Buffer.from(`${records.join("\0")}\0`) }),
|
||||
);
|
||||
expect(
|
||||
report.modinfo?.entries.map(({ key, value, raw_hex }) => ({
|
||||
key,
|
||||
value,
|
||||
raw_hex,
|
||||
})),
|
||||
).toEqual(
|
||||
records.map((record) => ({
|
||||
key: record.slice(0, record.indexOf("=")),
|
||||
value: record.slice(record.indexOf("=") + 1),
|
||||
raw_hex: Buffer.from(`${record}\0`).toString("hex"),
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
it("distinguishes absent metadata from unavailable section names", () => {
|
||||
const absent = parse(kernelModuleFixture({ modinfo: null, versions: null }));
|
||||
expect(absent.modinfo?.status).toBe("absent");
|
||||
expect(absent.versions?.status).toBe("absent");
|
||||
const bytes = kernelModuleFixture();
|
||||
bytes.writeUInt16LE(0, 62);
|
||||
const unknown = parse(bytes);
|
||||
expect(unknown.modinfo?.status).toBe("unknown");
|
||||
expect(unknown.versions?.status).toBe("unknown");
|
||||
});
|
||||
|
||||
it.each([
|
||||
[64, true],
|
||||
[64, false],
|
||||
[32, true],
|
||||
[32, false],
|
||||
] as const)("reports an ET_REL ELF%s %s module", (bits, little) => {
|
||||
const report = parse(kernelModuleFixture({ bits, little }));
|
||||
const width = bits === 64 ? 16 : 8;
|
||||
expect(report).toMatchObject({
|
||||
format: bits === 64 ? "elf64" : "elf32",
|
||||
byte_order: little ? "little" : "big",
|
||||
machine: bits === 64 ? 62 : 20,
|
||||
type: 1,
|
||||
type_name: "relocatable",
|
||||
applicability: "applicable",
|
||||
compression: null,
|
||||
next_probe: null,
|
||||
coverage: { status: "complete", examined_entries: 9 },
|
||||
});
|
||||
expect(
|
||||
report.modinfo?.entries.map((entry) => [entry.key, entry.value]),
|
||||
).toEqual([
|
||||
["license", "GPL"],
|
||||
["vermagic", "5.15.0 SMP mod_unload"],
|
||||
["alias", "pci:v1"],
|
||||
["alias", "pci:v2"],
|
||||
["name", "demo"],
|
||||
["depends", ""],
|
||||
]);
|
||||
expect(
|
||||
report.modinfo?.entries.every((entry) => entry.status === "observed"),
|
||||
).toBe(true);
|
||||
expect(report.versions?.layout).toBe(
|
||||
bits === 64 ? "elf64_unsigned_long_name56" : "elf32_unsigned_long_name60",
|
||||
);
|
||||
expect(
|
||||
report.versions?.entries.map((entry) => [entry.crc_hex, entry.name]),
|
||||
).toEqual([
|
||||
["12345678".padStart(width, "0"), "printk"],
|
||||
["90abcdef".padStart(width, "0"), "module_layout"],
|
||||
]);
|
||||
expect(report.this_module).toMatchObject({
|
||||
status: "present",
|
||||
layout: "unknown",
|
||||
location: { bytes: 8 },
|
||||
});
|
||||
expect(report.ksymtab?.sections.map((section) => section.name)).toEqual([
|
||||
"__ksymtab",
|
||||
]);
|
||||
expect(report.init_text).toMatchObject({ status: "present", bytes: 4 });
|
||||
expect(report.exit_text).toMatchObject({ status: "present", bytes: 1 });
|
||||
expect(report.signature).toMatchObject({
|
||||
status: "present",
|
||||
reason: null,
|
||||
pkcs7: {
|
||||
location: { bytes: pkcs7.length },
|
||||
sha256: pkcs7Sha256,
|
||||
},
|
||||
info: { algo: 0, hash: 0, id_type: 2, sig_len: pkcs7.length },
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps earlier .modinfo entries when the last span is unterminated", () => {
|
||||
const report = parse(
|
||||
kernelModuleFixture({
|
||||
modinfo: Buffer.from("license=GPL\0alias=pci:v3", "latin1"),
|
||||
}),
|
||||
);
|
||||
expect(report.modinfo?.status).toBe("present");
|
||||
expect(report.modinfo?.entries.map((entry) => entry.reason)).toEqual([
|
||||
null,
|
||||
"unterminated_modinfo_entry",
|
||||
]);
|
||||
expect(report.modinfo?.entries[1]).toMatchObject({
|
||||
status: "malformed",
|
||||
key: "alias",
|
||||
value: "pci:v3",
|
||||
});
|
||||
});
|
||||
|
||||
it("reports a missing separator, an empty key, and an over-long field per entry", () => {
|
||||
const report = parse(
|
||||
kernelModuleFixture({
|
||||
modinfo: Buffer.concat([
|
||||
Buffer.from("license\0=GPL\0", "latin1"),
|
||||
Buffer.from(`${"k".repeat(257)}=x\0`, "latin1"),
|
||||
Buffer.from(`name=${"v".repeat(4097)}\0`, "latin1"),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
expect(report.modinfo?.entries.map((entry) => entry.reason)).toEqual([
|
||||
"modinfo_missing_separator",
|
||||
"modinfo_empty_key",
|
||||
"modinfo_field_exceeds_limit",
|
||||
"modinfo_field_exceeds_limit",
|
||||
]);
|
||||
expect(report.modinfo?.entries[1]).toMatchObject({
|
||||
key: null,
|
||||
value: "GPL",
|
||||
});
|
||||
expect(report.modinfo?.entries[2]).toMatchObject({ key: null, value: "x" });
|
||||
expect(report.modinfo?.entries[3]).toMatchObject({
|
||||
key: "name",
|
||||
value: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("closes __versions when the section size is not a multiple of 64", () => {
|
||||
const report = parse(kernelModuleFixture({ versions: Buffer.alloc(63) }));
|
||||
expect(report.versions).toMatchObject({
|
||||
status: "malformed",
|
||||
entries: [],
|
||||
reason: "versions_size_not_multiple",
|
||||
layout: "elf64_unsigned_long_name56",
|
||||
entry_bytes: 64,
|
||||
});
|
||||
expect(report.coverage.examined_entries).toBe(
|
||||
(report.modinfo?.entries.length ?? 0) +
|
||||
(report.ksymtab?.sections.length ?? 0),
|
||||
);
|
||||
});
|
||||
|
||||
it("observes a version name with nonzero padding and rejects a missing NUL", () => {
|
||||
const padded = modversionEntry(0x12345678, "printk", 64, true);
|
||||
padded[8 + "printk".length + 1] = 0x7f;
|
||||
const unterminated = Buffer.alloc(64, 0x41);
|
||||
unterminated.writeBigUInt64LE(0x11n, 0);
|
||||
const report = parse(
|
||||
kernelModuleFixture({
|
||||
versions: Buffer.concat([padded, unterminated]),
|
||||
}),
|
||||
);
|
||||
expect(report.versions?.entries[0]).toMatchObject({
|
||||
status: "observed",
|
||||
name: "printk",
|
||||
reason: null,
|
||||
crc_hex: "0000000012345678",
|
||||
});
|
||||
expect(report.versions?.entries[1]).toMatchObject({
|
||||
status: "malformed",
|
||||
name: null,
|
||||
reason: "version_name_unterminated",
|
||||
crc_hex: "0000000000000011",
|
||||
});
|
||||
});
|
||||
|
||||
it("reports an inconsistent signature length without a PKCS#7 range", () => {
|
||||
const report = parse(
|
||||
kernelModuleFixture({
|
||||
signature: moduleSignatureTrailer(Buffer.from("abcd"), 50_000),
|
||||
}),
|
||||
);
|
||||
expect(report.signature).toMatchObject({
|
||||
status: "malformed",
|
||||
reason: "signature_length_inconsistent",
|
||||
pkcs7: null,
|
||||
info: { sig_len: 50_000, id_type: 2 },
|
||||
});
|
||||
expect(report.modinfo?.entries[0]?.key).toBe("license");
|
||||
});
|
||||
|
||||
it("withholds PKCS#7 bytes for nonzero signature parameters and a non-PKCS7 id", () => {
|
||||
const nonzero = Buffer.alloc(12);
|
||||
nonzero[0] = 1;
|
||||
nonzero[2] = 2;
|
||||
nonzero.writeUInt32BE(4, 8);
|
||||
const otherId = Buffer.alloc(12);
|
||||
otherId[0] = 1;
|
||||
otherId[3] = 2;
|
||||
otherId[2] = 1;
|
||||
otherId.writeUInt32BE(4, 8);
|
||||
const magic = Buffer.from(MODULE_SIGNATURE_MAGIC, "latin1");
|
||||
const payload = Buffer.from("abcd");
|
||||
expect(
|
||||
parse(
|
||||
kernelModuleFixture({
|
||||
signature: Buffer.concat([payload, nonzero, magic]),
|
||||
}),
|
||||
).signature,
|
||||
).toMatchObject({
|
||||
status: "malformed",
|
||||
reason: "signature_parameters_nonzero",
|
||||
pkcs7: null,
|
||||
});
|
||||
expect(
|
||||
parse(
|
||||
kernelModuleFixture({
|
||||
signature: Buffer.concat([payload, otherId, magic]),
|
||||
}),
|
||||
).signature,
|
||||
).toMatchObject({
|
||||
status: "unsupported",
|
||||
reason: "signature_id_type_not_pkcs7",
|
||||
pkcs7: null,
|
||||
info: { id_type: 1 },
|
||||
});
|
||||
});
|
||||
|
||||
it("does not examine kernel metadata on a non-relocatable ELF", () => {
|
||||
const report = parse(kernelModuleFixture({ type: 2 }));
|
||||
expect(report).toMatchObject({
|
||||
applicability: "not_applicable",
|
||||
type: 2,
|
||||
type_name: "executable",
|
||||
modinfo: null,
|
||||
versions: null,
|
||||
signature: null,
|
||||
coverage: { status: "not_applicable", examined_entries: 0 },
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["gzip", "xz", "zstd"] as const)(
|
||||
"recommends external decompression for a %s container",
|
||||
(codec) => {
|
||||
const report = parse(compressedKernelModuleContainer(codec));
|
||||
expect(report).toMatchObject({
|
||||
applicability: "unsupported",
|
||||
format: null,
|
||||
compression: { codec, status: "unsupported" },
|
||||
next_probe: { operation: "external_decompression" },
|
||||
modinfo: null,
|
||||
coverage: { status: "unsupported", examined_entries: 0 },
|
||||
});
|
||||
expect(report.next_probe?.rationale).toContain(codec);
|
||||
expect(report.next_probe?.rationale).toContain("external");
|
||||
expect(report.next_probe?.rationale).toContain(
|
||||
"call inspect_kernel_module on the decompressed file",
|
||||
);
|
||||
expect(
|
||||
kernelModuleSchema.safeParse({
|
||||
...report,
|
||||
next_probe: { ...report.next_probe, operation: "extract_artifact" },
|
||||
}).success,
|
||||
).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it("fails closed when the combined entry budget is exhausted", () => {
|
||||
expect(() => parse(kernelModuleFixture(), { max_entries: 7 })).toThrow(
|
||||
ArtifactReaderFailure,
|
||||
);
|
||||
try {
|
||||
parse(kernelModuleFixture(), { max_entries: 7 });
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(ArtifactReaderFailure);
|
||||
expect(error).toMatchObject({ reason: "limit", boundary: "capacity" });
|
||||
}
|
||||
});
|
||||
|
||||
it("examines the first of two .modinfo sections and records the duplicate", () => {
|
||||
const report = parse(kernelModuleFixture({ repeatModinfo: true }));
|
||||
expect(report.modinfo?.entries).toHaveLength(6);
|
||||
expect(report.limitations.join("\n")).toContain(
|
||||
"Multiple .modinfo sections are declared; the first is examined.",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not decode compressed, nobits, or out-of-file metadata sections", () => {
|
||||
const compressed = kernelModuleFixture();
|
||||
compressed.writeBigUInt64LE(0x800n, 64 + 64 + 8);
|
||||
expect(parse(compressed).modinfo).toMatchObject({
|
||||
status: "unsupported",
|
||||
entries: [],
|
||||
reason: "modinfo_section_compressed",
|
||||
});
|
||||
const nobits = kernelModuleFixture();
|
||||
nobits.writeUInt32LE(8, 64 + 64 + 4);
|
||||
expect(parse(nobits).modinfo).toMatchObject({
|
||||
status: "malformed",
|
||||
location: null,
|
||||
entries: [],
|
||||
reason: "modinfo_section_not_file_backed",
|
||||
});
|
||||
const outside = kernelModuleFixture();
|
||||
outside.writeBigUInt64LE(1_000_000n, 64 + 64 + 24);
|
||||
expect(parse(outside).modinfo).toMatchObject({
|
||||
status: "malformed",
|
||||
reason: "modinfo_section_not_file_backed",
|
||||
});
|
||||
const versionsOutside = kernelModuleFixture();
|
||||
versionsOutside.writeBigUInt64LE(1_000_000n, 64 + 64 * 2 + 24);
|
||||
expect(parse(versionsOutside).versions).toMatchObject({
|
||||
status: "malformed",
|
||||
entries: [],
|
||||
reason: "versions_section_not_file_backed",
|
||||
});
|
||||
const versionsCompressed = kernelModuleFixture();
|
||||
versionsCompressed.writeBigUInt64LE(0x800n, 64 + 64 * 2 + 8);
|
||||
expect(parse(versionsCompressed).versions).toMatchObject({
|
||||
status: "unsupported",
|
||||
entries: [],
|
||||
reason: "versions_section_compressed",
|
||||
});
|
||||
const versionsNobits = kernelModuleFixture();
|
||||
versionsNobits.writeUInt32LE(8, 64 + 64 * 2 + 4);
|
||||
expect(parse(versionsNobits).versions).toMatchObject({
|
||||
status: "malformed",
|
||||
location: null,
|
||||
entries: [],
|
||||
reason: "versions_section_not_file_backed",
|
||||
});
|
||||
const moduleSection = kernelModuleFixture();
|
||||
moduleSection.writeUInt32LE(8, 64 + 64 * 3 + 4);
|
||||
expect(parse(moduleSection).this_module).toMatchObject({
|
||||
status: "present",
|
||||
location: null,
|
||||
layout: "unknown",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects a non-ELF file that is not a recognized container", () => {
|
||||
expect(() => parse(Buffer.from("not an elf"))).toThrow(BinaryFormatFailure);
|
||||
});
|
||||
|
||||
it("rejects ELF bytes preceding a bare signature magic as an inconsistent trailer", () => {
|
||||
const report = parse(
|
||||
kernelModuleFixture({
|
||||
signature: Buffer.from(MODULE_SIGNATURE_MAGIC, "latin1"),
|
||||
}),
|
||||
);
|
||||
expect(report.signature).toMatchObject({
|
||||
status: "malformed",
|
||||
reason: "signature_length_inconsistent",
|
||||
pkcs7: null,
|
||||
});
|
||||
expect(report.modinfo?.entries[0]?.key).toBe("license");
|
||||
});
|
||||
|
||||
it("records a missing trailer as observed absence", () => {
|
||||
const report = parse(kernelModuleFixture({ signature: null }));
|
||||
expect(report.signature).toMatchObject({
|
||||
status: "absent",
|
||||
magic: null,
|
||||
info: null,
|
||||
pkcs7: null,
|
||||
reason: null,
|
||||
});
|
||||
expect(report.limitations.join("\n")).toContain(
|
||||
"not proof the module is unsigned",
|
||||
);
|
||||
});
|
||||
|
||||
it("names a non-file-backed __ksymtab section without dropping the module", () => {
|
||||
const bytes = kernelModuleFixture();
|
||||
bytes.writeUInt32LE(8, 64 + 64 * 4 + 4);
|
||||
const report = parse(bytes);
|
||||
expect(report.ksymtab?.sections).toEqual([]);
|
||||
expect(report.limitations.join("\n")).toContain(
|
||||
"1 __ksymtab* section(s) are declared but not file-backed",
|
||||
);
|
||||
expect(report.modinfo?.entries[0]?.key).toBe("license");
|
||||
});
|
||||
|
||||
it("charges many sections sharing one huge __ksymtab name to the entry budget", () => {
|
||||
const name = `__ksymtab_${"x".repeat(65_490)}`;
|
||||
const bytes = kernelModuleFixture({
|
||||
sharedNameSections: { name, count: 8192 },
|
||||
});
|
||||
const started = performance.now();
|
||||
expect(() => parse(bytes)).toThrow(ArtifactReaderFailure);
|
||||
const report = parse(bytes, { max_entries: 65_536 });
|
||||
expect(performance.now() - started).toBeLessThan(5_000);
|
||||
const huge = report.ksymtab?.sections.slice(1) ?? [];
|
||||
expect(huge).toHaveLength(8192);
|
||||
expect(
|
||||
huge.every(
|
||||
(section) =>
|
||||
section.name === null &&
|
||||
section.reason === "ksymtab_name_exceeds_limit",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(report.ksymtab?.sections[0]).toMatchObject({
|
||||
name: "__ksymtab",
|
||||
reason: null,
|
||||
});
|
||||
expect(report.coverage.examined_entries).toBe(6 + 2 + 1 + 8192);
|
||||
expect(JSON.stringify(report).length).toBeLessThan(2 * 1024 * 1024);
|
||||
});
|
||||
@@ -0,0 +1,634 @@
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
|
||||
import { elfObjectTypeName } from "../../domain/native/elfNotes.js";
|
||||
import {
|
||||
KERNEL_MODULE_VERSION_ENTRY_BYTES,
|
||||
MAX_KERNEL_MODULE_KEY_BYTES,
|
||||
MAX_KERNEL_MODULE_RAW_BYTES,
|
||||
MAX_KERNEL_MODULE_SECTION_NAME_BYTES,
|
||||
MAX_KERNEL_MODULE_VALUE_BYTES,
|
||||
MODULE_SIGNATURE_INFO_BYTES,
|
||||
MODULE_SIGNATURE_MAGIC,
|
||||
MODULE_SIGNATURE_PKCS7_ID_TYPE,
|
||||
type KernelModule,
|
||||
type InspectKernelModuleInput,
|
||||
} from "../../domain/native/kernelModule.js";
|
||||
import type {
|
||||
ImageSection,
|
||||
BinaryContainer,
|
||||
} from "../metadata/BinaryContainer.js";
|
||||
import { readElfImage } from "../metadata/ElfImage.js";
|
||||
|
||||
type ElfSection = Extract<ImageSection, { format: "elf" }>;
|
||||
type Report = KernelModule;
|
||||
type Limitations = string[];
|
||||
|
||||
const MAGIC = Buffer.from(MODULE_SIGNATURE_MAGIC, "latin1");
|
||||
const GZIP = Buffer.from([0x1f, 0x8b]);
|
||||
const XZ = Buffer.from([0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00]);
|
||||
const ZSTD = Buffer.from([0x28, 0xb5, 0x2f, 0xfd]);
|
||||
|
||||
const sha256 = (bytes: Buffer): string =>
|
||||
createHash("sha256").update(bytes).digest("hex");
|
||||
|
||||
const exceeds = (examined: number, maxEntries: number): void => {
|
||||
if (examined > maxEntries)
|
||||
throw new ArtifactReaderFailure(
|
||||
"limit",
|
||||
`Kernel module entries exceed the ${String(maxEntries)}-entry budget.`,
|
||||
{ boundary: "capacity" },
|
||||
);
|
||||
};
|
||||
|
||||
const fits = (offset: bigint, size: bigint, fileLength: number): boolean =>
|
||||
offset >= 0n &&
|
||||
size >= 0n &&
|
||||
offset <= BigInt(fileLength) &&
|
||||
size <= BigInt(fileLength) - offset;
|
||||
|
||||
const locationOf = (
|
||||
section: ElfSection,
|
||||
): { offset: number; bytes: number } | null => {
|
||||
if (
|
||||
!fits(
|
||||
section.declared_range.offset,
|
||||
section.declared_range.size,
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
)
|
||||
)
|
||||
return null;
|
||||
return {
|
||||
offset: Number(section.declared_range.offset),
|
||||
bytes: Number(section.declared_range.size),
|
||||
};
|
||||
};
|
||||
|
||||
const inFile = (section: ElfSection, fileLength: number): boolean =>
|
||||
fits(section.declared_range.offset, section.declared_range.size, fileLength);
|
||||
|
||||
const rawHex = (bytes: Buffer): string | null =>
|
||||
bytes.length > MAX_KERNEL_MODULE_RAW_BYTES ? null : bytes.toString("hex");
|
||||
|
||||
const compressionCodec = (bytes: Buffer): Report["compression"] => {
|
||||
if (bytes.subarray(0, GZIP.length).equals(GZIP))
|
||||
return { codec: "gzip", status: "unsupported" };
|
||||
if (bytes.length >= XZ.length && bytes.subarray(0, XZ.length).equals(XZ))
|
||||
return { codec: "xz", status: "unsupported" };
|
||||
if (
|
||||
bytes.length >= ZSTD.length &&
|
||||
bytes.subarray(0, ZSTD.length).equals(ZSTD)
|
||||
)
|
||||
return { codec: "zstd", status: "unsupported" };
|
||||
return null;
|
||||
};
|
||||
|
||||
const baseLimitations = (): Limitations => [
|
||||
"This workflow does not execute the module, resolve imported symbols, or validate an appended signature.",
|
||||
"struct module in .gnu.linkonce.this_module is kernel-version specific and is not decoded.",
|
||||
"__ksymtab entry layout is kernel-version specific and is not decoded.",
|
||||
];
|
||||
|
||||
const headerFacts = (
|
||||
bytes: Buffer,
|
||||
image: BinaryContainer,
|
||||
): Pick<Report, "format" | "byte_order" | "machine" | "type" | "type_name"> => {
|
||||
const little = image.byte_order === "little";
|
||||
const type = little ? bytes.readUInt16LE(16) : bytes.readUInt16BE(16);
|
||||
return {
|
||||
format: image.bits === 64 ? "elf64" : "elf32",
|
||||
byte_order: image.byte_order,
|
||||
machine: image.format === "elf" ? image.elf.machine : null,
|
||||
type,
|
||||
type_name: elfObjectTypeName(type),
|
||||
};
|
||||
};
|
||||
|
||||
const unsupportedCompression = (
|
||||
artifact: Report["artifact"],
|
||||
compression: NonNullable<Report["compression"]>,
|
||||
): Report => ({
|
||||
artifact,
|
||||
format: null,
|
||||
byte_order: null,
|
||||
machine: null,
|
||||
type: null,
|
||||
type_name: null,
|
||||
applicability: "unsupported",
|
||||
compression,
|
||||
next_probe: {
|
||||
operation: "external_decompression",
|
||||
rationale: `Decompress this ${compression.codec} container with an external ${compression.codec} utility into a separate file, then call inspect_kernel_module on the decompressed file. REA does not decode ${compression.codec} containers.`,
|
||||
},
|
||||
modinfo: null,
|
||||
versions: null,
|
||||
this_module: null,
|
||||
ksymtab: null,
|
||||
init_text: null,
|
||||
exit_text: null,
|
||||
signature: null,
|
||||
coverage: { status: "unsupported", examined_entries: 0 },
|
||||
limitations: [
|
||||
...baseLimitations(),
|
||||
"gzip, xz, and zstd kernel-module containers are not decoded. The existing ELF section decoder does not cover these whole-file codecs.",
|
||||
"A compressed container is not proof the payload is a kernel module.",
|
||||
],
|
||||
});
|
||||
|
||||
const notApplicable = (
|
||||
artifact: Report["artifact"],
|
||||
facts: ReturnType<typeof headerFacts>,
|
||||
): Report => ({
|
||||
artifact,
|
||||
...facts,
|
||||
applicability: "not_applicable",
|
||||
compression: null,
|
||||
next_probe: null,
|
||||
modinfo: null,
|
||||
versions: null,
|
||||
this_module: null,
|
||||
ksymtab: null,
|
||||
init_text: null,
|
||||
exit_text: null,
|
||||
signature: null,
|
||||
coverage: { status: "not_applicable", examined_entries: 0 },
|
||||
limitations: [
|
||||
...baseLimitations(),
|
||||
"Kernel module metadata is defined for ET_REL objects. This ELF type is not applicable, so .modinfo, __versions, and the signature trailer were not examined.",
|
||||
],
|
||||
});
|
||||
|
||||
interface SectionUse {
|
||||
readonly namesComplete: boolean;
|
||||
readonly limitations: Limitations;
|
||||
}
|
||||
|
||||
const sectionsNamed = (
|
||||
sections: readonly ElfSection[],
|
||||
name: string,
|
||||
): readonly ElfSection[] => sections.filter((section) => section.name === name);
|
||||
|
||||
const parseModinfo = (
|
||||
bytes: Buffer,
|
||||
section: ElfSection | undefined,
|
||||
use: SectionUse,
|
||||
examined: { count: number },
|
||||
maxEntries: number,
|
||||
): Report["modinfo"] => {
|
||||
if (section === undefined)
|
||||
return {
|
||||
status: use.namesComplete ? "absent" : "unknown",
|
||||
location: null,
|
||||
sha256: null,
|
||||
entries: [],
|
||||
reason: null,
|
||||
};
|
||||
const location = locationOf(section);
|
||||
if (section.type === 8 || location === null || !inFile(section, bytes.length))
|
||||
return {
|
||||
status: "malformed",
|
||||
location: null,
|
||||
sha256: null,
|
||||
entries: [],
|
||||
reason: "modinfo_section_not_file_backed",
|
||||
};
|
||||
const stored = bytes.subarray(
|
||||
location.offset,
|
||||
location.offset + location.bytes,
|
||||
);
|
||||
if (section.compressed)
|
||||
return {
|
||||
status: "unsupported",
|
||||
location,
|
||||
sha256: sha256(stored),
|
||||
entries: [],
|
||||
reason: "modinfo_section_compressed",
|
||||
};
|
||||
const entries: NonNullable<Report["modinfo"]>["entries"] = [];
|
||||
let at = 0;
|
||||
while (at < stored.length) {
|
||||
if (stored[at] === 0) {
|
||||
at += 1;
|
||||
continue;
|
||||
}
|
||||
const end = stored.indexOf(0, at);
|
||||
const terminated = end >= 0;
|
||||
const stop = terminated ? end : stored.length;
|
||||
const record = stored.subarray(at, stop);
|
||||
const raw = stored.subarray(at, terminated ? end + 1 : stored.length);
|
||||
examined.count += 1;
|
||||
exceeds(examined.count, maxEntries);
|
||||
const separator = record.indexOf(0x3d);
|
||||
let key: string | null = null;
|
||||
let value: string | null = null;
|
||||
let reason: NonNullable<Report["modinfo"]>["entries"][number]["reason"] =
|
||||
terminated ? null : "unterminated_modinfo_entry";
|
||||
if (separator < 0) reason ??= "modinfo_missing_separator";
|
||||
else {
|
||||
const keyBytes = record.subarray(0, separator);
|
||||
const valueBytes = record.subarray(separator + 1);
|
||||
if (keyBytes.length === 0) reason ??= "modinfo_empty_key";
|
||||
if (
|
||||
keyBytes.length > MAX_KERNEL_MODULE_KEY_BYTES ||
|
||||
valueBytes.length > MAX_KERNEL_MODULE_VALUE_BYTES
|
||||
)
|
||||
reason ??= "modinfo_field_exceeds_limit";
|
||||
if (keyBytes.length > 0 && keyBytes.length <= MAX_KERNEL_MODULE_KEY_BYTES)
|
||||
key = keyBytes.toString("latin1");
|
||||
if (valueBytes.length <= MAX_KERNEL_MODULE_VALUE_BYTES)
|
||||
value = valueBytes.toString("latin1");
|
||||
}
|
||||
entries.push({
|
||||
index: entries.length,
|
||||
location: {
|
||||
offset: location.offset + at,
|
||||
bytes: raw.length,
|
||||
},
|
||||
key,
|
||||
value,
|
||||
raw_sha256: sha256(raw),
|
||||
raw_hex: rawHex(raw),
|
||||
status: reason === null ? "observed" : "malformed",
|
||||
reason,
|
||||
});
|
||||
if (!terminated) break;
|
||||
at = end + 1;
|
||||
}
|
||||
return {
|
||||
status: "present",
|
||||
location,
|
||||
sha256: sha256(stored),
|
||||
entries,
|
||||
reason: null,
|
||||
};
|
||||
};
|
||||
|
||||
const parseVersions = (
|
||||
bytes: Buffer,
|
||||
section: ElfSection | undefined,
|
||||
bits: 32 | 64,
|
||||
little: boolean,
|
||||
use: SectionUse,
|
||||
examined: { count: number },
|
||||
maxEntries: number,
|
||||
): Report["versions"] => {
|
||||
const layout =
|
||||
bits === 64 ? "elf64_unsigned_long_name56" : "elf32_unsigned_long_name60";
|
||||
const entryBytes = KERNEL_MODULE_VERSION_ENTRY_BYTES;
|
||||
if (section === undefined)
|
||||
return {
|
||||
status: use.namesComplete ? "absent" : "unknown",
|
||||
layout,
|
||||
entry_bytes: entryBytes,
|
||||
location: null,
|
||||
sha256: null,
|
||||
entries: [],
|
||||
reason: null,
|
||||
};
|
||||
const location = locationOf(section);
|
||||
if (section.type === 8 || location === null || !inFile(section, bytes.length))
|
||||
return {
|
||||
status: "malformed",
|
||||
layout,
|
||||
entry_bytes: entryBytes,
|
||||
location: null,
|
||||
sha256: null,
|
||||
entries: [],
|
||||
reason: "versions_section_not_file_backed",
|
||||
};
|
||||
const stored = bytes.subarray(
|
||||
location.offset,
|
||||
location.offset + location.bytes,
|
||||
);
|
||||
if (section.compressed)
|
||||
return {
|
||||
status: "unsupported",
|
||||
layout,
|
||||
entry_bytes: entryBytes,
|
||||
location,
|
||||
sha256: sha256(stored),
|
||||
entries: [],
|
||||
reason: "versions_section_compressed",
|
||||
};
|
||||
if (stored.length % entryBytes !== 0)
|
||||
return {
|
||||
status: "malformed",
|
||||
layout,
|
||||
entry_bytes: entryBytes,
|
||||
location,
|
||||
sha256: sha256(stored),
|
||||
entries: [],
|
||||
reason: "versions_size_not_multiple",
|
||||
};
|
||||
const nameBytes = bits === 64 ? 56 : 60;
|
||||
const crcBytes = bits === 64 ? 8 : 4;
|
||||
const count = stored.length / entryBytes;
|
||||
exceeds(examined.count + count, maxEntries);
|
||||
const entries: NonNullable<Report["versions"]>["entries"] = [];
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const start = index * entryBytes;
|
||||
const record = stored.subarray(start, start + entryBytes);
|
||||
const crc =
|
||||
bits === 64
|
||||
? little
|
||||
? record.readBigUInt64LE(0)
|
||||
: record.readBigUInt64BE(0)
|
||||
: BigInt(little ? record.readUInt32LE(0) : record.readUInt32BE(0));
|
||||
const nameField = record.subarray(crcBytes, crcBytes + nameBytes);
|
||||
const end = nameField.indexOf(0);
|
||||
entries.push({
|
||||
index,
|
||||
location: { offset: location.offset + start, bytes: entryBytes },
|
||||
crc_hex: crc.toString(16).padStart(crcBytes * 2, "0"),
|
||||
name: end < 0 ? null : nameField.subarray(0, end).toString("latin1"),
|
||||
status: end < 0 ? "malformed" : "observed",
|
||||
reason: end < 0 ? "version_name_unterminated" : null,
|
||||
});
|
||||
}
|
||||
examined.count += count;
|
||||
return {
|
||||
status: "present",
|
||||
layout,
|
||||
entry_bytes: entryBytes,
|
||||
location,
|
||||
sha256: sha256(stored),
|
||||
entries,
|
||||
reason: null,
|
||||
};
|
||||
};
|
||||
|
||||
const presence = (
|
||||
section: ElfSection | undefined,
|
||||
namesComplete: boolean,
|
||||
fileLength: number,
|
||||
): {
|
||||
readonly status: "present" | "absent" | "unknown" | "unsupported";
|
||||
readonly location: { offset: number; bytes: number } | null;
|
||||
} => {
|
||||
if (section === undefined)
|
||||
return { status: namesComplete ? "absent" : "unknown", location: null };
|
||||
if (section.type === 8 || !inFile(section, fileLength))
|
||||
return { status: "present", location: null };
|
||||
const location = locationOf(section);
|
||||
if (section.compressed) return { status: "unsupported", location };
|
||||
return { status: "present", location };
|
||||
};
|
||||
|
||||
const sized = (
|
||||
bytes: Buffer,
|
||||
section: ElfSection | undefined,
|
||||
namesComplete: boolean,
|
||||
limitations: Limitations,
|
||||
label: string,
|
||||
): Report["init_text"] => {
|
||||
if (section === undefined)
|
||||
return {
|
||||
status: namesComplete ? "absent" : "unknown",
|
||||
location: null,
|
||||
bytes: null,
|
||||
compressed: null,
|
||||
};
|
||||
if (!inFile(section, bytes.length) || section.type === 8) {
|
||||
limitations.push(
|
||||
`${label} is declared but not file-backed, so its size is unknown.`,
|
||||
);
|
||||
return {
|
||||
status: "present",
|
||||
location: null,
|
||||
bytes: null,
|
||||
compressed: null,
|
||||
};
|
||||
}
|
||||
const location = locationOf(section);
|
||||
if (section.compressed)
|
||||
limitations.push(
|
||||
`${label} is stored compressed. The reported size is the file-backed storage size; the decoded size is unknown.`,
|
||||
);
|
||||
return {
|
||||
status: "present",
|
||||
location,
|
||||
bytes: location?.bytes ?? null,
|
||||
compressed: section.compressed,
|
||||
};
|
||||
};
|
||||
|
||||
const parseSignature = (bytes: Buffer): Report["signature"] => {
|
||||
if (
|
||||
bytes.length < MAGIC.length ||
|
||||
!bytes.subarray(bytes.length - MAGIC.length).equals(MAGIC)
|
||||
)
|
||||
return {
|
||||
status: "absent",
|
||||
magic: null,
|
||||
info: null,
|
||||
pkcs7: null,
|
||||
reason: null,
|
||||
};
|
||||
const magic = {
|
||||
offset: bytes.length - MAGIC.length,
|
||||
bytes: MAGIC.length,
|
||||
};
|
||||
if (magic.offset < MODULE_SIGNATURE_INFO_BYTES)
|
||||
return {
|
||||
status: "malformed",
|
||||
magic,
|
||||
info: null,
|
||||
pkcs7: null,
|
||||
reason: "signature_trailer_truncated",
|
||||
};
|
||||
const infoOffset = magic.offset - MODULE_SIGNATURE_INFO_BYTES;
|
||||
const info = {
|
||||
location: { offset: infoOffset, bytes: MODULE_SIGNATURE_INFO_BYTES },
|
||||
algo: bytes[infoOffset] ?? 0,
|
||||
hash: bytes[infoOffset + 1] ?? 0,
|
||||
id_type: bytes[infoOffset + 2] ?? 0,
|
||||
signer_len: bytes[infoOffset + 3] ?? 0,
|
||||
key_id_len: bytes[infoOffset + 4] ?? 0,
|
||||
pad: [
|
||||
bytes[infoOffset + 5] ?? 0,
|
||||
bytes[infoOffset + 6] ?? 0,
|
||||
bytes[infoOffset + 7] ?? 0,
|
||||
] as [number, number, number],
|
||||
sig_len: bytes.readUInt32BE(infoOffset + 8),
|
||||
};
|
||||
if (info.sig_len >= infoOffset)
|
||||
return {
|
||||
status: "malformed",
|
||||
magic,
|
||||
info,
|
||||
pkcs7: null,
|
||||
reason: "signature_length_inconsistent",
|
||||
};
|
||||
if (info.id_type !== MODULE_SIGNATURE_PKCS7_ID_TYPE)
|
||||
return {
|
||||
status: "unsupported",
|
||||
magic,
|
||||
info,
|
||||
pkcs7: null,
|
||||
reason: "signature_id_type_not_pkcs7",
|
||||
};
|
||||
const parametersNonzero =
|
||||
info.algo !== 0 ||
|
||||
info.hash !== 0 ||
|
||||
info.signer_len !== 0 ||
|
||||
info.key_id_len !== 0 ||
|
||||
info.pad.some((value) => value !== 0);
|
||||
if (parametersNonzero)
|
||||
return {
|
||||
status: "malformed",
|
||||
magic,
|
||||
info,
|
||||
pkcs7: null,
|
||||
reason: "signature_parameters_nonzero",
|
||||
};
|
||||
const pkcs7 = bytes.subarray(infoOffset - info.sig_len, infoOffset);
|
||||
return {
|
||||
status: "present",
|
||||
magic,
|
||||
info,
|
||||
pkcs7: {
|
||||
location: { offset: infoOffset - info.sig_len, bytes: info.sig_len },
|
||||
sha256: sha256(pkcs7),
|
||||
},
|
||||
reason: null,
|
||||
};
|
||||
};
|
||||
|
||||
const signatureLimitation = (signature: Report["signature"]): string => {
|
||||
if (signature?.status === "absent")
|
||||
return "No '~Module signature appended~' trailer was present. That is not proof the module is unsigned in another container.";
|
||||
if (signature?.status === "present")
|
||||
return "PKCS#7 bytes are identified by the module_signature trailer and hashed. Certificate chain, signer, and digest coverage are unobserved.";
|
||||
return "The appended signature trailer was not accepted as an unvalidated PKCS#7 range.";
|
||||
};
|
||||
|
||||
/** Decode kernel-module metadata from one in-memory ET_REL image. */
|
||||
export const parseKernelModule = (
|
||||
bytes: Buffer,
|
||||
artifact: Report["artifact"],
|
||||
input: InspectKernelModuleInput,
|
||||
): Report => {
|
||||
const compression = compressionCodec(bytes);
|
||||
if (compression !== null)
|
||||
return unsupportedCompression(artifact, compression);
|
||||
const image = readElfImage(bytes);
|
||||
const facts = headerFacts(bytes, image);
|
||||
if (facts.type !== 1) return notApplicable(artifact, facts);
|
||||
const sections = image.sections.filter(
|
||||
(section): section is ElfSection => section.format === "elf",
|
||||
);
|
||||
const namesComplete =
|
||||
image.section_names_limitation === undefined &&
|
||||
sections.every((section) => section.name !== null);
|
||||
const limitations = baseLimitations();
|
||||
if (image.section_names_limitation !== undefined)
|
||||
limitations.push(image.section_names_limitation);
|
||||
const use = { namesComplete, limitations };
|
||||
const examined = { count: 0 };
|
||||
const noteDuplicates = (name: string, found: readonly ElfSection[]): void => {
|
||||
if (found.length > 1)
|
||||
limitations.push(
|
||||
`Multiple ${name} sections are declared; the first is examined.`,
|
||||
);
|
||||
};
|
||||
const modinfoSections = sectionsNamed(sections, ".modinfo");
|
||||
const versionSections = sectionsNamed(sections, "__versions");
|
||||
const moduleSections = sectionsNamed(sections, ".gnu.linkonce.this_module");
|
||||
noteDuplicates(".modinfo", modinfoSections);
|
||||
noteDuplicates("__versions", versionSections);
|
||||
noteDuplicates(".gnu.linkonce.this_module", moduleSections);
|
||||
const modinfo = parseModinfo(
|
||||
bytes,
|
||||
modinfoSections[0],
|
||||
use,
|
||||
examined,
|
||||
input.max_entries,
|
||||
);
|
||||
const versions = parseVersions(
|
||||
bytes,
|
||||
versionSections[0],
|
||||
image.bits,
|
||||
image.byte_order === "little",
|
||||
use,
|
||||
examined,
|
||||
input.max_entries,
|
||||
);
|
||||
const modulePresence = presence(
|
||||
moduleSections[0],
|
||||
namesComplete,
|
||||
bytes.length,
|
||||
);
|
||||
const ksymtabNamed = sections.filter(
|
||||
(section) => section.name?.startsWith("__ksymtab") === true,
|
||||
);
|
||||
const ksymtabSections = ksymtabNamed.filter(
|
||||
(section) => section.type !== 8 && inFile(section, bytes.length),
|
||||
);
|
||||
const unbacked = ksymtabNamed.length - ksymtabSections.length;
|
||||
if (unbacked > 0)
|
||||
limitations.push(
|
||||
`${String(unbacked)} __ksymtab* section(s) are declared but not file-backed, so their ranges are unknown.`,
|
||||
);
|
||||
const ksymtab = ksymtabSections.flatMap((section) => {
|
||||
const location = locationOf(section);
|
||||
if (section.name === null || location === null) return [];
|
||||
const fitsName =
|
||||
section.name.length <= MAX_KERNEL_MODULE_SECTION_NAME_BYTES;
|
||||
return [
|
||||
{
|
||||
name: fitsName ? section.name : null,
|
||||
location,
|
||||
compressed: section.compressed,
|
||||
reason: fitsName ? null : ("ksymtab_name_exceeds_limit" as const),
|
||||
},
|
||||
];
|
||||
});
|
||||
// Each listed section repeats its name, so the list shares the entry budget.
|
||||
examined.count += ksymtab.length;
|
||||
exceeds(examined.count, input.max_entries);
|
||||
if (!namesComplete)
|
||||
limitations.push(
|
||||
"Section names are incomplete, so a missing .modinfo, __versions, this_module, __ksymtab, .init.text, or .exit.text section is unknown rather than absent.",
|
||||
);
|
||||
const signature = parseSignature(bytes);
|
||||
if (modinfo?.status === "absent")
|
||||
limitations.push("No .modinfo section was declared.");
|
||||
if (versions?.status === "absent")
|
||||
limitations.push("No __versions section was declared.");
|
||||
limitations.push(signatureLimitation(signature));
|
||||
return {
|
||||
artifact,
|
||||
...facts,
|
||||
applicability: "applicable",
|
||||
compression: null,
|
||||
next_probe: null,
|
||||
modinfo,
|
||||
versions,
|
||||
this_module: {
|
||||
status: modulePresence.status,
|
||||
location: modulePresence.location,
|
||||
layout: "unknown",
|
||||
},
|
||||
ksymtab: {
|
||||
status: namesComplete ? "observed" : "unknown",
|
||||
sections: ksymtab,
|
||||
},
|
||||
init_text: sized(
|
||||
bytes,
|
||||
sectionsNamed(sections, ".init.text")[0],
|
||||
namesComplete,
|
||||
limitations,
|
||||
".init.text",
|
||||
),
|
||||
exit_text: sized(
|
||||
bytes,
|
||||
sectionsNamed(sections, ".exit.text")[0],
|
||||
namesComplete,
|
||||
limitations,
|
||||
".exit.text",
|
||||
),
|
||||
signature,
|
||||
coverage: { status: "complete", examined_entries: examined.count },
|
||||
limitations,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,249 @@
|
||||
import { isSelectedRegularFileError } from "../../filesystem/RegularFile.js";
|
||||
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
|
||||
import { readStableArtifact } from "../../artifacts/readStableArtifact.js";
|
||||
import {
|
||||
artifactFilePrimaryFacts,
|
||||
type ArtifactFilePrimaryFacts,
|
||||
bindArtifactFileCleanup,
|
||||
artifactFileCancellationCause,
|
||||
} from "../../artifacts/ArtifactFileOperation.js";
|
||||
import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
|
||||
import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js";
|
||||
import type { KernelModulePort } from "../../application/binaryDiagnostics/KernelModulePort.js";
|
||||
import type { ExecutionOptions } from "../../application/AnalysisProvider.js";
|
||||
import { ArtifactOperationError } from "../../domain/artifactOperationError.js";
|
||||
import {
|
||||
AnalysisAccessDeniedError,
|
||||
AnalysisArtifactChangedError,
|
||||
AnalysisCancelledError,
|
||||
AnalysisInputError,
|
||||
AnalysisResourceConstraintError,
|
||||
AnalysisUnsupportedTargetError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { fixedAllowanceOptions } from "../../domain/fixedAllowanceRemediation.js";
|
||||
import {
|
||||
MAX_INSPECT_KERNEL_MODULE_ENTRIES,
|
||||
MAX_INSPECT_KERNEL_MODULE_FILE_BYTES,
|
||||
type InspectKernelModuleInput,
|
||||
} from "../../domain/native/kernelModule.js";
|
||||
import { err, ok } from "../../domain/result.js";
|
||||
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
|
||||
import { KERNEL_MODULE_PROVIDER } from "../../domain/investigationProviders.js";
|
||||
import {
|
||||
BinaryFormatFailure,
|
||||
BinaryResourceFailure,
|
||||
} from "../metadata/BinaryContainer.js";
|
||||
import { parseKernelModule } from "./KernelModuleParser.js";
|
||||
|
||||
const OPERATION = "inspect_kernel_module";
|
||||
|
||||
/** Portable parser over one bounded, identity-checked regular file. */
|
||||
export class KernelModuleProvider implements KernelModulePort {
|
||||
readonly identity = KERNEL_MODULE_PROVIDER;
|
||||
readonly #resources = new ArtifactResourceScope();
|
||||
|
||||
async close(): Promise<void> {
|
||||
try {
|
||||
await this.#resources.close();
|
||||
} catch (cause: unknown) {
|
||||
if (
|
||||
!(cause instanceof ArtifactReaderFailure) ||
|
||||
cause.cleanup === undefined
|
||||
)
|
||||
throw cause;
|
||||
const error = new ProviderCleanupError(
|
||||
KERNEL_MODULE_PROVIDER.id,
|
||||
cause.cleanup.resources,
|
||||
{ reason: cause.cleanup.reason },
|
||||
{ cause, operation: OPERATION },
|
||||
);
|
||||
bindArtifactFileCleanup(cause, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async inspect(input: InspectKernelModuleInput, options?: ExecutionOptions) {
|
||||
try {
|
||||
return await this.#resources.run(async () => {
|
||||
const snapshot = await readStableArtifact(
|
||||
input.path,
|
||||
input.max_file_bytes,
|
||||
options?.signal,
|
||||
undefined,
|
||||
this.#resources,
|
||||
);
|
||||
return ok(
|
||||
parseKernelModule(
|
||||
snapshot.bytes,
|
||||
{
|
||||
path: input.path,
|
||||
sha256: snapshot.sha256,
|
||||
bytes: snapshot.bytes.length,
|
||||
},
|
||||
input,
|
||||
),
|
||||
);
|
||||
});
|
||||
} catch (cause) {
|
||||
return err(kernelModuleFailure(cause, input));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const TEXT_BUDGET =
|
||||
"The kernel-module entry text and hash budget fixed by the admitted image length";
|
||||
|
||||
const inputBudgetRemediation = (
|
||||
boundary: "file-size" | "capacity",
|
||||
input: InspectKernelModuleInput,
|
||||
): string =>
|
||||
boundary === "file-size"
|
||||
? input.max_file_bytes >= MAX_INSPECT_KERNEL_MODULE_FILE_BYTES
|
||||
? `The image exceeds the ${String(MAX_INSPECT_KERNEL_MODULE_FILE_BYTES)}-byte input ceiling this operation accepts; select a smaller artifact.`
|
||||
: `Raise max_file_bytes above ${String(input.max_file_bytes)} bytes or select a smaller artifact; the limit is the caller-selected input read budget.`
|
||||
: input.max_entries >= MAX_INSPECT_KERNEL_MODULE_ENTRIES
|
||||
? `The image exceeds the ${String(MAX_INSPECT_KERNEL_MODULE_ENTRIES)}-entry kernel-module ceiling this operation accepts; select a smaller artifact.`
|
||||
: `Raise max_entries above ${String(input.max_entries)} or select a smaller artifact; the limit is the caller-selected .modinfo and __versions entry budget.`;
|
||||
|
||||
const kernelModuleFailurePrimary = (
|
||||
facts: ArtifactFilePrimaryFacts,
|
||||
input: InspectKernelModuleInput,
|
||||
) => {
|
||||
const { cause, origin } = facts;
|
||||
if (cause instanceof BinaryResourceFailure)
|
||||
return new AnalysisResourceConstraintError(
|
||||
OPERATION,
|
||||
"memory",
|
||||
cause.message,
|
||||
{ boundary: cause.boundary, maximum_bytes: cause.maximum_bytes },
|
||||
{
|
||||
cause,
|
||||
...fixedAllowanceOptions(
|
||||
"The ELF structural decoding allowance",
|
||||
"parser",
|
||||
),
|
||||
},
|
||||
);
|
||||
if (cause instanceof BinaryFormatFailure)
|
||||
return cause.kind === "unsupported"
|
||||
? new AnalysisUnsupportedTargetError(
|
||||
OPERATION,
|
||||
input.path,
|
||||
cause.message,
|
||||
{ cause },
|
||||
)
|
||||
: new AnalysisInputError(OPERATION, { cause }, [
|
||||
{
|
||||
path: ["path"],
|
||||
reason: "invalid_format",
|
||||
message: cause.message,
|
||||
},
|
||||
]);
|
||||
if (
|
||||
origin === "owned-or-classified" &&
|
||||
cause instanceof ArtifactReaderFailure &&
|
||||
cause.reason === "integrity"
|
||||
)
|
||||
return new AnalysisArtifactChangedError(
|
||||
OPERATION,
|
||||
input.path,
|
||||
cause.message,
|
||||
{
|
||||
cause,
|
||||
},
|
||||
);
|
||||
if (
|
||||
origin === "owned-or-classified" &&
|
||||
cause instanceof ArtifactReaderFailure &&
|
||||
cause.reason === "cancelled"
|
||||
)
|
||||
return new AnalysisCancelledError(OPERATION, {
|
||||
cause: artifactFileCancellationCause(cause),
|
||||
});
|
||||
if (
|
||||
origin === "owned-or-classified" &&
|
||||
cause instanceof ArtifactReaderFailure
|
||||
) {
|
||||
if (cause.reason === "limit") {
|
||||
// Caller-selected read and entry budgets name the bound to raise.
|
||||
// Entry text and hashes are bounded by the admitted image, so host
|
||||
// memory cannot raise that ceiling.
|
||||
const textBudget = cause.boundary === "memory";
|
||||
return new AnalysisResourceConstraintError(
|
||||
OPERATION,
|
||||
cause.boundary ?? "memory",
|
||||
cause.message,
|
||||
{
|
||||
max_file_bytes: input.max_file_bytes,
|
||||
max_entries: input.max_entries,
|
||||
},
|
||||
{
|
||||
cause,
|
||||
...(cause.boundary === undefined
|
||||
? {}
|
||||
: textBudget
|
||||
? fixedAllowanceOptions(TEXT_BUDGET, "parser")
|
||||
: {
|
||||
retryable: false as const,
|
||||
remediationAction: inputBudgetRemediation(
|
||||
cause.boundary,
|
||||
input,
|
||||
),
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
if (cause.reason === "path" || cause.reason === "format")
|
||||
return new AnalysisInputError(OPERATION, { cause }, [
|
||||
{
|
||||
path: ["path"],
|
||||
reason: "invalid_format",
|
||||
message: cause.message,
|
||||
},
|
||||
]);
|
||||
}
|
||||
if (cause instanceof Error && "code" in cause) {
|
||||
if (cause.code === "EACCES" || cause.code === "EPERM")
|
||||
return new AnalysisAccessDeniedError(OPERATION, input.path, cause.code, {
|
||||
cause,
|
||||
});
|
||||
if (isSelectedRegularFileError(cause))
|
||||
return new AnalysisInputError(OPERATION, { cause }, [
|
||||
{
|
||||
path: ["path"],
|
||||
reason: "invalid_value",
|
||||
message: `Selected kernel module could not be read (${String(cause.code)}): ${input.path}.`,
|
||||
},
|
||||
]);
|
||||
}
|
||||
return new ArtifactOperationError(
|
||||
OPERATION,
|
||||
origin === "owned-or-classified" && cause instanceof ArtifactReaderFailure
|
||||
? cause.reason
|
||||
: "io",
|
||||
undefined,
|
||||
cause instanceof Error ? cause.message : "Kernel module inspection failed.",
|
||||
{ cause },
|
||||
);
|
||||
};
|
||||
|
||||
const kernelModuleFailure = (
|
||||
cause: unknown,
|
||||
input: InspectKernelModuleInput,
|
||||
) => {
|
||||
const primary = kernelModuleFailurePrimary(
|
||||
artifactFilePrimaryFacts(cause),
|
||||
input,
|
||||
);
|
||||
if (!(cause instanceof ArtifactReaderFailure) || cause.cleanup === undefined)
|
||||
return primary;
|
||||
const cleanup = new ProviderCleanupError(
|
||||
KERNEL_MODULE_PROVIDER.id,
|
||||
cause.cleanup.resources,
|
||||
{ reason: cause.cleanup.reason },
|
||||
{ operation: OPERATION, cause },
|
||||
);
|
||||
const error = analysisErrorWithCleanupFailure(primary, cleanup, OPERATION);
|
||||
bindArtifactFileCleanup(cause, error);
|
||||
return error;
|
||||
};
|
||||
@@ -25,6 +25,7 @@ import { captureProviderEnvironment } from "./providerEnvironment.js";
|
||||
import {
|
||||
createElfNotesService,
|
||||
createAppleIpsService,
|
||||
createKernelModuleService,
|
||||
createPeLoadConfigService,
|
||||
createPeCertificatesService,
|
||||
createPeImportsService,
|
||||
@@ -33,6 +34,7 @@ import {
|
||||
} from "../composition/binaryDiagnostics.js";
|
||||
import type { ElfNotesService } from "../application/binaryDiagnostics/ElfNotesService.js";
|
||||
import type { AppleIpsService } from "../application/binaryDiagnostics/AppleIpsService.js";
|
||||
import type { KernelModuleService } from "../application/binaryDiagnostics/KernelModuleService.js";
|
||||
import type { PeLoadConfigService } from "../application/binaryDiagnostics/PeLoadConfigService.js";
|
||||
import type { PeCertificatesService } from "../application/binaryDiagnostics/PeCertificatesService.js";
|
||||
import type { PeImportsService } from "../application/binaryDiagnostics/PeImportsService.js";
|
||||
@@ -40,6 +42,7 @@ import type { PeResourcesService } from "../application/binaryDiagnostics/PeReso
|
||||
import type { NativeExportsService } from "../application/binaryDiagnostics/NativeExportsService.js";
|
||||
import { registerElfNotesTool } from "./registerElfNotesTool.js";
|
||||
import { registerAppleIpsTool } from "./registerAppleIpsTool.js";
|
||||
import { registerKernelModuleTool } from "./registerKernelModuleTool.js";
|
||||
import { registerPeLoadConfigTool } from "./registerPeLoadConfigTool.js";
|
||||
import { registerPeCertificatesTool } from "./registerPeCertificatesTool.js";
|
||||
import { registerPeImportsTool } from "./registerPeImportsTool.js";
|
||||
@@ -477,6 +480,7 @@ export const createServer = (
|
||||
const nativeExports = createNativeExportsService();
|
||||
const elfNotes = createElfNotesService();
|
||||
const appleIps = createAppleIpsService();
|
||||
const kernelModule = createKernelModuleService();
|
||||
const peLoadConfig = createPeLoadConfigService();
|
||||
const peCertificates = createPeCertificatesService();
|
||||
const peImports = createPeImportsService();
|
||||
@@ -591,6 +595,7 @@ export const createServer = (
|
||||
nativeExports.close(),
|
||||
elfNotes.close(),
|
||||
appleIps.close(),
|
||||
kernelModule.close(),
|
||||
peLoadConfig.close(),
|
||||
peCertificates.close(),
|
||||
peImports.close(),
|
||||
@@ -662,6 +667,7 @@ export const createServer = (
|
||||
nativeExports,
|
||||
elfNotes,
|
||||
appleIps,
|
||||
kernelModule,
|
||||
peLoadConfig,
|
||||
peCertificates,
|
||||
peImports,
|
||||
@@ -902,6 +908,7 @@ const registerConfiguredAnalysisTools = (
|
||||
nativeExports,
|
||||
elfNotes,
|
||||
appleIps,
|
||||
kernelModule,
|
||||
peLoadConfig,
|
||||
peCertificates,
|
||||
peImports,
|
||||
@@ -978,6 +985,13 @@ const registerConfiguredAnalysisTools = (
|
||||
recordEvidence,
|
||||
withAdmittedAnalysis,
|
||||
);
|
||||
registerKernelModuleTool(
|
||||
server,
|
||||
kernelModule,
|
||||
toolLogger,
|
||||
recordEvidence,
|
||||
withAdmittedAnalysis,
|
||||
);
|
||||
registerPeLoadConfigTool(
|
||||
server,
|
||||
peLoadConfig,
|
||||
@@ -1156,6 +1170,7 @@ interface ServerToolContext extends ReturnType<typeof createSessionRecorders> {
|
||||
readonly nativeExports: NativeExportsService;
|
||||
readonly elfNotes: ElfNotesService;
|
||||
readonly appleIps: AppleIpsService;
|
||||
readonly kernelModule: KernelModuleService;
|
||||
readonly peLoadConfig: PeLoadConfigService;
|
||||
readonly peCertificates: PeCertificatesService;
|
||||
readonly peImports: PeImportsService;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
|
||||
import type { KernelModuleService } from "../application/binaryDiagnostics/KernelModuleService.js";
|
||||
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
|
||||
import { toolContract } from "../contracts/toolContracts.js";
|
||||
import type { Logger } from "pino";
|
||||
import { createEvidenceToolHandler } from "./contractToolHandler.js";
|
||||
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
|
||||
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
|
||||
|
||||
/** Register the explicit-file Linux kernel module workflow and retain its complete Evidence. */
|
||||
export const registerKernelModuleTool = (
|
||||
server: EvidenceMcpServer,
|
||||
service: KernelModuleService,
|
||||
logger: Logger,
|
||||
recordEvidence?: EvidenceWriter["recordEvidence"],
|
||||
withAdmittedAnalysis?: WithAdmittedAnalysis,
|
||||
): void => {
|
||||
const contract = toolContract("inspect_kernel_module");
|
||||
const handler = createEvidenceToolHandler(
|
||||
server,
|
||||
(input, options) => service.inspect(input, options),
|
||||
logger,
|
||||
recordEvidence,
|
||||
withAdmittedAnalysis,
|
||||
);
|
||||
server.registerTool(
|
||||
contract.name,
|
||||
toolRegistrationOptions(contract),
|
||||
handler(contract),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect } from "vitest";
|
||||
import { toolContract } from "../../src/contracts/toolContracts.js";
|
||||
import {
|
||||
compressedKernelModuleContainer,
|
||||
kernelModuleFixture,
|
||||
} from "../../src/native/elf/KernelModule.fixture.js";
|
||||
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../support/cli/cliFixture.js";
|
||||
|
||||
cliTest(
|
||||
"reports license, duplicate aliases, and version CRCs through the public CLI",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-kernel-module-cli-");
|
||||
const path = join(root, "demo.ko");
|
||||
await writeFile(path, kernelModuleFixture());
|
||||
const result = await cli.run({
|
||||
arguments: ["inspect-kernel-module", path, "--json"],
|
||||
});
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
const evidence = toolContract("inspect_kernel_module").outputSchema.parse(
|
||||
result.json,
|
||||
);
|
||||
expect(
|
||||
evidence.normalized_result.modinfo?.entries.map((entry) => [
|
||||
entry.key,
|
||||
entry.value,
|
||||
]),
|
||||
).toEqual([
|
||||
["license", "GPL"],
|
||||
["vermagic", "5.15.0 SMP mod_unload"],
|
||||
["alias", "pci:v1"],
|
||||
["alias", "pci:v2"],
|
||||
["name", "demo"],
|
||||
["depends", ""],
|
||||
]);
|
||||
expect(evidence.normalized_result.versions?.entries[0]?.crc_hex).toBe(
|
||||
"0000000012345678",
|
||||
);
|
||||
expect(evidence.normalized_result.signature?.status).toBe("present");
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"resolves a relative kernel module path against the working directory",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-kernel-module-cli-rel-");
|
||||
await writeFile(join(root, "demo.ko"), kernelModuleFixture({ type: 3 }));
|
||||
const result = await cli.run({
|
||||
arguments: ["inspect-kernel-module", "demo.ko", "--json"],
|
||||
cwd: root,
|
||||
});
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
const evidence = toolContract("inspect_kernel_module").outputSchema.parse(
|
||||
result.json,
|
||||
);
|
||||
expect(evidence.normalized_result.applicability).toBe("not_applicable");
|
||||
expect(evidence.normalized_result.type_name).toBe("shared");
|
||||
expect(evidence.subject?.local_path).toBe(join(root, "demo.ko"));
|
||||
},
|
||||
);
|
||||
|
||||
cliTest("reports a non-ELF file as invalid input", async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-kernel-module-cli-bad-");
|
||||
const path = join(root, "broken.ko");
|
||||
await writeFile(path, Buffer.from("not an elf"));
|
||||
const result = await cli.run({
|
||||
arguments: ["inspect-kernel-module", path, "--json"],
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.json).toMatchObject({
|
||||
code: "invalid_request",
|
||||
details: { operation: "inspect_kernel_module" },
|
||||
});
|
||||
});
|
||||
|
||||
cliTest(
|
||||
"reports a compressed container without claiming the payload is a module",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-kernel-module-cli-xz-");
|
||||
const path = join(root, "demo.ko.xz");
|
||||
await writeFile(path, compressedKernelModuleContainer("xz"));
|
||||
const result = await cli.run({
|
||||
arguments: ["inspect-kernel-module", path, "--json"],
|
||||
});
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
const evidence = toolContract("inspect_kernel_module").outputSchema.parse(
|
||||
result.json,
|
||||
);
|
||||
expect(evidence.normalized_result).toMatchObject({
|
||||
applicability: "unsupported",
|
||||
format: null,
|
||||
compression: { codec: "xz", status: "unsupported" },
|
||||
next_probe: { operation: "external_decompression" },
|
||||
modinfo: null,
|
||||
});
|
||||
expect(evidence.subject?.format).toBe("file");
|
||||
},
|
||||
);
|
||||
@@ -16,6 +16,7 @@ const fileCommands = [
|
||||
"decode-signal-recording",
|
||||
"inspect-model-artifact",
|
||||
"inspect-elf-notes",
|
||||
"inspect-kernel-module",
|
||||
"inspect-byte-entropy",
|
||||
"inspect-pe-resources",
|
||||
"inspect-pe-certificates",
|
||||
|
||||
@@ -225,6 +225,7 @@ const pathRequests: readonly [string, Record<string, unknown>][] = [
|
||||
["inspect_native_exports", { path: nul }],
|
||||
["inspect_elf_notes", { path: nul }],
|
||||
["inspect_apple_ips", { path: nul }],
|
||||
["inspect_kernel_module", { path: nul }],
|
||||
["inspect_pe_load_config", { path: nul }],
|
||||
["inspect_python_pyc", { path: nul }],
|
||||
["inspect_erlang_beam", { path: nul }],
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { symlink, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createKernelModuleService } from "../../../src/composition/binaryDiagnostics.js";
|
||||
import {
|
||||
compressedKernelModuleContainer,
|
||||
kernelModuleFixture,
|
||||
} from "../../../src/native/elf/KernelModule.fixture.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
describe("kernel module stable filesystem boundary", () => {
|
||||
it("reports non-file and malformed ELF selections as caller input errors", async () => {
|
||||
const root = await createTestTempDirectory("rea-kernel-module-files-");
|
||||
const path = join(root, "demo.ko");
|
||||
const alias = join(root, "alias");
|
||||
const malformed = join(root, "malformed");
|
||||
await writeFile(path, kernelModuleFixture());
|
||||
await writeFile(malformed, Buffer.from("not an elf"));
|
||||
await symlink(path, alias);
|
||||
const service = createKernelModuleService();
|
||||
try {
|
||||
for (const [candidate, reason] of [
|
||||
[root, "invalid_format"],
|
||||
[alias, "invalid_format"],
|
||||
[malformed, "invalid_format"],
|
||||
[join(root, "missing"), "invalid_value"],
|
||||
[join(path, "child"), "invalid_value"],
|
||||
] as const) {
|
||||
const result = await service.inspect({ path: candidate });
|
||||
if (result.ok) throw new Error("Expected file acquisition failure");
|
||||
expect(result.error).toMatchObject({
|
||||
_tag: "AnalysisInputError",
|
||||
operation: "inspect_kernel_module",
|
||||
issues: [
|
||||
{
|
||||
path: ["path"],
|
||||
reason,
|
||||
message: expect.any(String),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
await service.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("returns typed resource and cancellation failures", async () => {
|
||||
const root = await createTestTempDirectory("rea-kernel-module-budget-");
|
||||
const path = join(root, "demo.ko");
|
||||
await writeFile(path, kernelModuleFixture());
|
||||
const service = createKernelModuleService();
|
||||
try {
|
||||
const tooSmall = await service.inspect({ path, max_file_bytes: 64 });
|
||||
if (tooSmall.ok) throw new Error("Expected resource budget failure");
|
||||
expect(tooSmall.error._tag).toBe("AnalysisResourceConstraintError");
|
||||
const tooMany = await service.inspect({ path, max_entries: 1 });
|
||||
if (tooMany.ok) throw new Error("Expected entry budget failure");
|
||||
expect(tooMany.error._tag).toBe("AnalysisResourceConstraintError");
|
||||
const cancelled = await service.inspect(
|
||||
{ path },
|
||||
{ signal: AbortSignal.abort() },
|
||||
);
|
||||
if (cancelled.ok) throw new Error("Expected cancellation");
|
||||
expect(cancelled.error._tag).toBe("AnalysisCancelledError");
|
||||
} finally {
|
||||
await service.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("reports a gzip container as unsupported without treating it as a module", async () => {
|
||||
const root = await createTestTempDirectory("rea-kernel-module-gzip-");
|
||||
const path = join(root, "demo.ko.gz");
|
||||
await writeFile(path, compressedKernelModuleContainer("gzip"));
|
||||
const service = createKernelModuleService();
|
||||
try {
|
||||
const result = await service.inspect({ path });
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.subject?.format).toBe("file");
|
||||
expect(result.value.normalized_result).toMatchObject({
|
||||
applicability: "unsupported",
|
||||
compression: { codec: "gzip", status: "unsupported" },
|
||||
next_probe: { operation: "external_decompression" },
|
||||
modinfo: null,
|
||||
});
|
||||
} finally {
|
||||
await service.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect } from "vitest";
|
||||
import { toolContract } from "../../../src/contracts/toolContracts.js";
|
||||
import { createServer } from "../../../src/server/createServer.js";
|
||||
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
||||
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
||||
import {
|
||||
compressedKernelModuleContainer,
|
||||
kernelModuleFixture,
|
||||
} from "../../../src/native/elf/KernelModule.fixture.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { mcpTest, type TestMcp } from "../../support/mcp/mcpFixture.js";
|
||||
|
||||
const connect = async (mcp: TestMcp) => {
|
||||
const session = createTestBinarySession(() => {
|
||||
throw new Error("Kernel module inspection must not start a deep provider");
|
||||
});
|
||||
const client = await mcp.connect(createServer({ kind: "session", session }));
|
||||
return { client, session };
|
||||
};
|
||||
|
||||
mcpTest(
|
||||
"advertises inspect_kernel_module and reads an ELF32 big-endian module",
|
||||
async ({ mcp }) => {
|
||||
const { client, session } = await connect(mcp);
|
||||
try {
|
||||
const advertised = (await client.listTools()).tools.find(
|
||||
(tool) => tool.name === "inspect_kernel_module",
|
||||
);
|
||||
expect(advertised?.annotations).toMatchObject({
|
||||
readOnlyHint: false,
|
||||
idempotentHint: true,
|
||||
});
|
||||
const root = await createTestTempDirectory("rea-kernel-module-mcp-");
|
||||
const path = join(root, "demo.ko");
|
||||
await writeFile(path, kernelModuleFixture({ bits: 32, little: false }));
|
||||
const response = await client.callTool({
|
||||
name: "inspect_kernel_module",
|
||||
arguments: { path },
|
||||
});
|
||||
expect(response.isError, JSON.stringify(response.content)).not.toBe(true);
|
||||
const evidence = toolContract("inspect_kernel_module").outputSchema.parse(
|
||||
response.structuredContent,
|
||||
);
|
||||
expect(evidence.normalized_result).toMatchObject({
|
||||
format: "elf32",
|
||||
byte_order: "big",
|
||||
machine: 20,
|
||||
applicability: "applicable",
|
||||
});
|
||||
expect(evidence.subject?.architecture).toBe("powerpc");
|
||||
expect(evidence.normalized_result.modinfo?.entries[0]).toMatchObject({
|
||||
key: "license",
|
||||
value: "GPL",
|
||||
});
|
||||
expect(evidence.normalized_result.versions?.entries[1]?.crc_hex).toBe(
|
||||
"90abcdef",
|
||||
);
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
mcpTest(
|
||||
"requires an absolute path for inspect_kernel_module",
|
||||
async ({ mcp }) => {
|
||||
const { client, session } = await connect(mcp);
|
||||
try {
|
||||
const response = await client.callTool({
|
||||
name: "inspect_kernel_module",
|
||||
arguments: { path: "demo.ko" },
|
||||
});
|
||||
expect(response.isError).toBe(true);
|
||||
expect(parseMcpToolError(response).error).toMatchObject({
|
||||
code: "invalid_request",
|
||||
details: { operation: "inspect_kernel_module" },
|
||||
});
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
mcpTest(
|
||||
"reports a zstd container as an unsupported kernel-module target",
|
||||
async ({ mcp }) => {
|
||||
const { client, session } = await connect(mcp);
|
||||
try {
|
||||
const root = await createTestTempDirectory("rea-kernel-module-zstd-mcp-");
|
||||
const path = join(root, "demo.ko.zst");
|
||||
await writeFile(path, compressedKernelModuleContainer("zstd"));
|
||||
const response = await client.callTool({
|
||||
name: "inspect_kernel_module",
|
||||
arguments: { path },
|
||||
});
|
||||
expect(response.isError, JSON.stringify(response.content)).not.toBe(true);
|
||||
const evidence = toolContract("inspect_kernel_module").outputSchema.parse(
|
||||
response.structuredContent,
|
||||
);
|
||||
expect(evidence.normalized_result).toMatchObject({
|
||||
applicability: "unsupported",
|
||||
compression: { codec: "zstd", status: "unsupported" },
|
||||
next_probe: { operation: "external_decompression" },
|
||||
});
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -99,12 +99,14 @@ import { PeResourcesProvider } from "../../src/native/pe/PeResourcesProvider.js"
|
||||
import { Il2cppMetadataProvider } from "../../src/il2cpp/Il2cppMetadataProvider.js";
|
||||
import { ElfNotesProvider } from "../../src/native/elf/ElfNotesProvider.js";
|
||||
import { AppleIpsProvider } from "../../src/native/apple/AppleIpsProvider.js";
|
||||
import { KernelModuleProvider } from "../../src/native/elf/KernelModuleProvider.js";
|
||||
import { PeCertificatesProvider } from "../../src/native/pe/PeCertificatesProvider.js";
|
||||
import { PeImportsProvider } from "../../src/native/pe/PeImportsProvider.js";
|
||||
import { NativeExportsProvider } from "../../src/native/exports/NativeExportsProvider.js";
|
||||
import { BeamProvider } from "../../src/erlang/BeamProvider.js";
|
||||
import { inspectElfNotesInputSchema } from "../../src/domain/native/elfNotes.js";
|
||||
import { inspectAppleIpsInputSchema } from "../../src/domain/native/appleIps.js";
|
||||
import { inspectKernelModuleInputSchema } from "../../src/domain/native/kernelModule.js";
|
||||
import { inspectPeCertificatesInputSchema } from "../../src/domain/native/peCertificates.js";
|
||||
import { inspectPeImportsInputSchema } from "../../src/domain/native/peImports.js";
|
||||
import { inspectNativeExportsInputSchema } from "../../src/domain/native/nativeExports.js";
|
||||
@@ -861,6 +863,7 @@ const stableArtifactProviders = [
|
||||
...staticProviders,
|
||||
["ELF notes", () => new ElfNotesProvider()],
|
||||
["Apple IPS", () => new AppleIpsProvider()],
|
||||
["kernel module", () => new KernelModuleProvider()],
|
||||
["PE certificates", () => new PeCertificatesProvider()],
|
||||
["PE load configuration", () => new PeLoadConfigProvider()],
|
||||
["PE imports", () => new PeImportsProvider()],
|
||||
@@ -894,6 +897,11 @@ const inspectFileFailure = (
|
||||
inspectAppleIpsInputSchema.parse({ path }),
|
||||
options,
|
||||
);
|
||||
if (provider instanceof KernelModuleProvider)
|
||||
return provider.inspect(
|
||||
inspectKernelModuleInputSchema.parse({ path }),
|
||||
options,
|
||||
);
|
||||
if (provider instanceof PeCertificatesProvider)
|
||||
return provider.inspect(
|
||||
inspectPeCertificatesInputSchema.parse({ path }),
|
||||
@@ -955,6 +963,7 @@ it.each(
|
||||
expect(result.error._tag).toBe(
|
||||
expectedTag ??
|
||||
(provider instanceof ElfNotesProvider ||
|
||||
provider instanceof KernelModuleProvider ||
|
||||
provider instanceof PeCertificatesProvider ||
|
||||
provider instanceof PeResourcesProvider
|
||||
? "ArtifactOperationError"
|
||||
|
||||
Reference in new issue
Block a user