-
Notifications
You must be signed in to change notification settings - Fork 166
GrumpyCats Library
Python client for the LitterBox HTTP API. Same surface as GrumpyCats CLI but importable β useful for batch operations, CI integrations, the LitterBoxMCP server, and anything that needs to drive LitterBox programmatically without shelling out.
from litterbox_client import LitterBoxClient
with LitterBoxClient("http://127.0.0.1:1337") as c:
info = c.upload_file("malware.exe")
static = c.analyze_file(info["md5"], "static", wait_for_completion=True)
risk = c.get_risk_assessment(info["md5"])The library lives in GrumpyCats/litterbox_client/ inside the repo. There's no separate PyPI package β install LitterBox's requirements.txt and add the GrumpyCats/ folder to PYTHONPATH, or just run scripts from GrumpyCats/:
cd GrumpyCats/
python my_script.pyIf you're running the CLI or MCP server from this folder, the import already works β LitterBoxMCP.py does sys.path.insert(0, Path(__file__).parent).
LitterBoxClient is a thin composition. Each domain lives in its own mixin under litterbox_client/:
class LitterBoxClient(
FilesMixin, # upload_file / upload_driver / delete_file
AnalysisMixin, # analyze_file / analyze_holygrail / validate_process
DoppelgangerMixin, # run_blender_scan / compare_with_blender / analyze_with_fuzzy
ResultsMixin, # get_static_results / get_dynamic_results / get_*
EdrMixin, # list_edr_profiles / analyze_edr / get_edr_results
ReportsMixin, # get_report / download_report
SystemMixin, # get_system_status / get_files_summary / cleanup
_BaseClient, # session, _make_request, retry policy
):
"""..."""Public callers import LitterBoxClient and treat it as one class. The split is purely an internal organization win.
LitterBoxClient(
base_url: str = "http://127.0.0.1:1337",
timeout: int = 120,
verify_ssl: bool = True,
proxy_config: dict | None = None, # e.g. {"http": "...", "https": "..."}
logger: logging.Logger | None = None,
)The client maintains a requests.Session for connection pooling. Use it as a context manager (with LitterBoxClient(...) as c:) or explicitly call c.close() to release the pool.
c.upload_file(path: str, file_name: str | None = None) -> dict
c.upload_and_analyze_file(path, analysis_types=("static", "dynamic"), file_name=None, cmd_args=None) -> dict
c.upload_driver(path: str, file_name: str | None = None) -> dict
c.upload_and_analyze_driver(path, file_name=None, run_holygrail=True) -> dict
c.delete_file(file_hash: str) -> dictupload_file returns {md5, size, original_name, ...}. Use md5 as the file_hash in subsequent calls.
c.analyze_file(target: str | int, analysis_type: str, cmd_args=None, wait_for_completion=True) -> dict
c.analyze_holygrail(file_hash: str, wait_for_completion=True) -> dict
c.validate_process(pid: int) -> dicttarget is an MD5 hash for files or an int PID for analysis_type='dynamic'. wait_for_completion=True is synchronous (the request blocks until the server returns final results).
c.list_edr_profiles() -> dict
c.get_edr_agents_status() -> dict # cached fleet probe
c.analyze_edr(file_hash, profile, cmd_args=None, xor_key=None) -> dict
c.get_edr_results(file_hash, profile) -> dict
c.get_edr_index(file_hash) -> dict
c.wait_for_edr_completion(file_hash, profile, interval=3.0, timeout=180.0) -> dict
c.fibratus_alerts_since(profile, since_iso, until_iso=None) -> dictanalyze_edr returns the Phase-1 result immediately β Phase-2 (alert correlation) runs in a server-side daemon thread. Either poll get_edr_results until the status is no longer polling_alerts, or call wait_for_edr_completion to block.
xor_key (0β255) requests XOR-on-the-wire encoding of the payload. Whiskers reverses the XOR while writing to disk; the cleartext window is bounded by the chunked-write buffer (64 KiB).
c.run_blender_scan() -> dict
c.compare_with_blender(file_hash) -> dict
c.analyze_with_fuzzy(file_hash, threshold=85) -> dict
c.create_fuzzy_database(folder_path, extensions=None) -> dictc.get_file_info(file_hash) -> dict
c.get_static_results(file_hash) -> dict
c.get_dynamic_results(target) -> dict
c.get_holygrail_results(file_hash) -> dict
c.get_risk_assessment(target) -> dict
c.get_comprehensive_results(target) -> dict # parallel fetch of every saved JSON
c.get_report(target) -> str # HTML inline
c.download_report(target, output_path=None) -> str # writes to disk, returns pathget_comprehensive_results fans out to file_info + static + dynamic + holygrail + EDR index in parallel via a thread pool β substantially faster than four sequential calls.
c.get_system_status(full=False) -> dict
c.get_files_summary() -> dict
c.get_scanners_status() -> dict
c.cleanup(include_uploads=True, include_results=True, include_analysis=True) -> dictTwo exception classes:
| Exception | When raised |
|---|---|
LitterBoxError |
Local validation, missing file, unsupported argument |
LitterBoxAPIError |
HTTP error from the server β has .status_code and .response_body
|
from litterbox_client import LitterBoxClient, LitterBoxAPIError
try:
c.analyze_edr(file_hash, "elastic", wait_for_completion=True)
except LitterBoxAPIError as e:
if e.status_code == 409:
print("agent busy β try again")
elif e.status_code == 502:
print("agent unreachable")
else:
raisefrom concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from litterbox_client import LitterBoxClient
samples = list(Path("./payloads").glob("*.exe"))
with LitterBoxClient("http://127.0.0.1:1337") as c:
profiles = [p["name"] for p in c.list_edr_profiles()["profiles"]]
# 1) upload everything
uploaded = [c.upload_file(str(p)) for p in samples]
# 2) fan out: each sample Γ each profile
def dispatch(sample, profile):
c.analyze_edr(sample["md5"], profile)
return c.wait_for_edr_completion(sample["md5"], profile, timeout=180)
with ThreadPoolExecutor(max_workers=8) as pool:
futures = {
pool.submit(dispatch, s, p): (s["md5"], p)
for s in uploaded for p in profiles
}
for fut in futures:
md5, profile = futures[fut]
result = fut.result()
print(md5, profile, result["status"], result["summary"]["total_alerts"])The Whiskers lock (single-occupancy) prevents two profiles from running on the same VM concurrently β but different profiles on different VMs run truly in parallel because Phase 2 doesn't hold the lock.
- GrumpyCats CLI β same surface, command-line
- LitterBoxMCP β same surface, exposed to LLMs
- HTTP API Reference β the underlying API
- GrumpyCats/litterbox_client/ β source
- π Home
- π§ Application Architecture
- π Dashboard
- π All in One Pipeline
- π― Detection Score Explained
- 𧬠Blender Scanner
- π FuzzyHash Scanner
- π‘οΈ HolyGrail BYOVD Scanner
- π YARA Rules Management