95 lines
3.1 KiB
Python
95 lines
3.1 KiB
Python
"""Behavioral signature generator.
|
|
|
|
This module implements a compact, deterministic behavioral fingerprint for a
|
|
compiled genome bundle. It expects two inputs:
|
|
|
|
- syscall_sequence: an iterable of syscall/call names observed or statically
|
|
extracted from the artifact (e.g. ["open","read","write","socket"]).
|
|
- static_imports: an iterable of import or feature strings (e.g. ["wasi::fd_read"]).
|
|
|
|
The generator computes n-gram sketches over the syscall sequence and a
|
|
feature-bit vector for imports, then produces a keyed HMAC-SHA256 over a
|
|
canonical JSON payload. The result is a compact hex string representing the
|
|
fingerprint.
|
|
|
|
The implementation aims to be small, deterministic, and fast so it can be used
|
|
in constrained admission controllers.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import hmac
|
|
import hashlib
|
|
import json
|
|
from collections import Counter, deque
|
|
from typing import Iterable, List, Tuple
|
|
|
|
DEFAULT_KEY = b"GenomeLoom::BehavioralSig::v1" # stable keyed hash
|
|
|
|
|
|
def _ngrams(seq: List[str], n: int) -> Iterable[Tuple[str, ...]]:
|
|
if n <= 0:
|
|
return []
|
|
dq = deque(maxlen=n)
|
|
out = []
|
|
for item in seq:
|
|
dq.append(item)
|
|
if len(dq) == n:
|
|
out.append(tuple(dq))
|
|
return out
|
|
|
|
|
|
def generate_behavioral_signature(
|
|
syscall_sequence: Iterable[str],
|
|
static_imports: Iterable[str] = (),
|
|
ngram_ns: Tuple[int, ...] = (1, 2, 3),
|
|
top_k: int = 32,
|
|
key: bytes = DEFAULT_KEY,
|
|
) -> str:
|
|
"""Generate a deterministic behavioral signature.
|
|
|
|
Returns a hex string (64 chars) representing HMAC-SHA256 over a canonical
|
|
payload. The payload contains:
|
|
- top n-gram counts (sorted)
|
|
- sorted list of imports
|
|
|
|
Parameters:
|
|
- syscall_sequence: iterable of syscall/call names
|
|
- static_imports: iterable of import feature strings
|
|
- ngram_ns: tuple sizes for n-grams
|
|
- top_k: how many n-grams to keep
|
|
- key: keyed hash secret (should be globally stable for the operator)
|
|
"""
|
|
seq = list(syscall_sequence)
|
|
# collect n-gram counts
|
|
counts = Counter()
|
|
for n in ngram_ns:
|
|
for ng in _ngrams(seq, n):
|
|
counts["+".join(ng)] += 1
|
|
|
|
# pick top-k by count then lexicographic for determinism
|
|
top = sorted(counts.items(), key=lambda kv: (-kv[1], kv[0]))[:top_k]
|
|
top_list = [(k, v) for k, v in top]
|
|
|
|
imports_sorted = sorted(set(static_imports))
|
|
|
|
payload = {"ngrams": top_list, "imports": imports_sorted}
|
|
canon = json.dumps(payload, separators=(",", ":"), sort_keys=True)
|
|
|
|
mac = hmac.new(key, canon.encode("utf-8"), hashlib.sha256).hexdigest()
|
|
return mac
|
|
|
|
|
|
def signature_summary(syscall_sequence: Iterable[str], static_imports: Iterable[str] = ()) -> dict:
|
|
"""Return a structured summary used in certificates (not the HMAC).
|
|
|
|
This includes top n-grams and feature import bits. Useful for human
|
|
inspection and sparse indexing.
|
|
"""
|
|
seq = list(syscall_sequence)
|
|
counts = Counter()
|
|
for n in (1, 2, 3):
|
|
for ng in _ngrams(seq, n):
|
|
counts["+".join(ng)] += 1
|
|
top = sorted(counts.items(), key=lambda kv: (-kv[1], kv[0]))[:32]
|
|
return {"ngrams": top, "imports": sorted(set(static_imports))}
|