Skip to content

feat: support the full Hatching Triage API - #13

Merged
gormaniac merged 7 commits into
mainfrom
claude/hatching-triage-sandbox-5i39vw
Nov 1, 2026
Merged

gormaniac merged 7 commits into
mainfrom
claude/hatching-triage-sandbox-5i39vw

Conversation

@gormaniac

@gormaniac gormaniac commented Oct 31, 2026 •

Copy link
Copy Markdown
Owner

Summary

Brings PyHatchingClient up to the whole Triage API, fixes several existing bugs, and adds a fake Triage server plus the first test suite. This is the client the new slib.hatching Storm service in gormaniac/stormsvcs is built on.

New endpoints

  • Samples: paged listing (owned / org / public) and search (samples_page, search_page, get_samples, iter_samples, iter_search, and search with a limit), delete_sample, set_sample_profile for interactive submissions, and fetch and import submissions.
  • Events: sample_events and samples_events stream status changes as NDJSON.
  • Reports: static_report, task_report, sample_summary, urlscan_report, and kernel_report, which picks the kernel log for the task's platform. overview now uses /v1, as the official clients do.
  • Files: task_file, task_pcap, task_screenshot, sample_archive (tar/zip), and chunked iter_* versions of every download.
  • Profiles and Yara: delete_profile, delete_rule, and update_profile now uses PUT. Profiles and submission defaults support the vpn network option with a geolocation.
  • Resources and stats: get_resources, get_geolocations, user_stats, and org_stats.
  • Magic links: magic_token for samples, plus get_org_settings and set_magic_link.
  • Users (private instances): get_users, get_user, create_user (username or email, with the API's name / role / email_confirmed fields), delete_user, get_user_apikeys, create_user_apikey, and delete_user_apikey.
  • Client options: PRIVATE_URL, plus api_path and trust_env.

Fixes

  • import pyhatching failed because enums was never imported.
  • The dependency pins were pydantic 1 / aiohttp 3.8.4, but the code already used pydantic v2 APIs. Now pydantic>=2.5,<3 and aiohttp>=3.9,<4.
  • Error responses with a 4xx/5xx status, including non-JSON bodies, now become an ErrorResponse (or a PyHatchingApiError when raise_on_api_err is set). Both carry status, error, and message.
  • ErrorNames gains NOT_AVAILABLE, which the live API returns for a report of a task that doesn't exist.
  • get_sample sent the hash instead of the resolved sample ID.
  • sample_id only returned an ID when a hash matched more than one sample.
  • URL submissions dropped every option except the URL.
  • File uploads from a path had no filename.
  • Credentials now uses the API's username / password fields. user / pass are still accepted.
  • Response models keep unknown fields instead of dropping them.
  • Hashes must now match the whole value, and ssdeep is supported.
  • CLI fixes for samples report, samples submit, and yara export.

Testing

  • pyhatching.testing.FakeTriage is a fake Triage server with canned data that records every request, now covering every endpoint above. Projects built on pyhatching can reuse it for their own tests.
  • tests/test_client.py covers every endpoint against it. CI now runs pytest.
  • tests/test_live.py is a live suite that runs against the real public API when HATCHING_TOKEN is set:
    • Read-only by default. It lists and searches public samples, pulls every report for a recent reported sample with a behavioral task, and downloads small files into memory. The public /samples listing leaves out tasks, so the suite fetches each candidate sample in full to find one.
    • HATCHING_LIVE_WRITE=1 adds the Yara rule and profile write tests. They create objects named pyhatching-livetest* and delete them afterwards.
    • HATCHING_LIVE_SUBMIT=1 resubmits one pinned sample, SUBMIT_SHA256 (59f22c39…dbbc7). It is already public on tria.ge and scored 10/10, so the test never makes a new file public. The test fails before submitting if the sample is missing, its score isn't 10, or the download doesn't match the hash. Each run adds a public sample that can't be deleted.
  • Live run against tria.ge, with writes and submission on: 45 passed (offline and live tests) and 1 skipped. The skip is org_stats, which the test account isn't allowed to read (UNAUTHORIZED).

Notes

  • Checked against the live docs: the endpoints were re-checked against the current public docs (tria.ge/docs/cloud-api/), which is where the resources, geolocations, stats, org settings, magic link, UrlScan, import kind, and VPN geolocation coverage came from.
  • Version: left at 0.3.1 so make release can do the bump.
  • Stale lock file: Pipfile.lock is out of date. pipenv install -d in CI will re-lock it.
  • Breaking changes:
    • search() takes limit (default 20) and pages through results, where it used to return only the first page.
    • ErrorResponse.error is now a str instead of an enum.
    • update_profile returns None on success.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KBLBzmCN8e76GcUB5rEPEL

Add every Triage endpoint to PyHatchingClient: sample listing (owned/org/public)
and search with pagination, sample deletion, interactive profile selection,
sample events, static/task/summary/kernel reports, task file, pcap, and archive
downloads (plus chunked iter_* variants), profile update/delete, Yara rule
deletion, and user/API key management for private instances.

Fixes:
- Use pydantic v2 and current aiohttp (the pins were pydantic 1 / aiohttp 3.8.4
  while the code already used pydantic v2 APIs).
- Import enums in the client module (the module failed to import).
- Handle non-2xx API errors, and non-JSON error bodies, as ErrorResponse /
  PyHatchingApiError (now carrying status, error, and message).
- get_sample used the hash instead of the resolved sample ID; sample_id only
  returned an ID when a hash had more than one sample.
- update_profile used POST (the API uses PUT) and parsed the wrong model.
- URL submissions dropped every option besides the URL; file uploads from a path
  had no filename; fetch submissions are now supported.
- The overview report uses the v1 endpoint, as the official clients do.
- Credentials use the API's username/password fields (user/pass still accepted).
- Response models keep unknown fields, and hashes must match the whole value
  (ssdeep is now supported).
- CLI fixes for the report, submit, and yara export commands.

Add pyhatching.testing.FakeTriage, a fake Triage API server, and a test suite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBLBzmMhxaREfczMZ8wa31
…r events end

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBLBzmMhxaREfczMZ8wa31
iter_sample, iter_task_file, iter_sample_archive, and kernel_report used the
sample argument as an ID, unlike their non-streaming versions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBLBzmMhxaREfczMZ8wa31
… suite

Rereading the public cloud API docs (tria.ge/docs/cloud-api/) surfaced
endpoints and fields pyhatching was missing:

- GET /resources (get_resources) and GET /geolocations (get_geolocations)
- GET /stats/user and /stats/org (user_stats, org_stats)
- GET /org/settings and PUT /org/settings/magic (get_org_settings,
  set_magic_link)
- GET /samples/{id}/magic (magic_token)
- report_urlscan.json (urlscan_report) and a task_screenshot helper
- the import submission kind, the vpn network option, and
  defaults.geolocation / profile geolocation fields
- user name/role/email_confirmed fields (first_name/last_name are
  deprecated by the API), username OR email on create_user
- newer search tags (clipper, collection, crypter, exploiter, fakeav,
  spam, spreader) and user management error names

FakeTriage serves all of the new endpoints and the fake suite covers them.

tests/test_live.py is a new live suite that runs against the real public
API when HATCHING_TOKEN is set (read-only by default; HATCHING_LIVE_WRITE
and HATCHING_LIVE_SUBMIT gate the mutating tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBLBzgsLYJhWB4Z8ZS1ejz
The public /samples listing omits tasks, so the live suite never found a
sample with a behavioral task and skipped every report test. Fetch the
full sample for candidates without tasks. The live API also answers an
unknown task report with NOT_AVAILABLE, so add it to ErrorNames and accept it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBLBzmCN8e76GcUB5rEPEL
Replace the benign example.com URL submission with a resubmission of a
recent reported public file sample whose Triage score is above 1 (not
clean). The sample is downloaded in memory, its sha256 checked against
the summary, and uploaded again as a new file sample.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBLBzmCN8e76GcUB5rEPEL
HATCHING_LIVE_SUBMIT now always resubmits SUBMIT_SHA256 (Triage score
10/10) instead of picking a recent flagged public sample, so the opt-in
test never makes a new file public. The test asserts the pinned sample
still exists with score 10 and that the download matches its sha256.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBLBzmCN8e76GcUB5rEPEL
@gormaniac
gormaniac merged commit 5155ab0 into main Nov 1, 2026
1 check passed
@gormaniac
gormaniac deleted the claude/hatching-triage-sandbox-5i39vw branch November 1, 2026 21:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants