Skip to main content

rl-cli

ReversingLabs CLI Client

Modular command-line client for ReversingLabs TitaniumCloud and A1000 malware analysis platforms

CI Status Python Versions License A1000 SDK Coverage

GitHub Stars GitHub Issues Buy Me a Coffee


Overview

rl-cli is a Python CLI and library that provides seamless access to ReversingLabs TitaniumCloud and A1000 malware analysis platforms, with Rich-formatted terminal output and full automation-friendly JSON/YAML modes.

Key Features

Feature Description
Malware Analysis Submit files for deep static and dynamic analysis
Multi-AV Scanning Detection results from 40+ antivirus engines
Network Intelligence URL, domain, and IP reputation and reports
Sample Management Upload, download, delete, reanalyze, and batch operations
YARA Integration Rulesets, online repositories, retro scans (cloud and local)
Classification & Tags Set and manage custom threat classifications
Broad A1000 SDK Coverage Wraps 54 of the 67 ReversingLabs A1000 SDK methods. Of the rest, four submit-and-report helpers are re-implemented here with better progress reporting, configuration_dump is a local string formatter replaced by config-dump, list_extracted_files_v2_aggregated pages for a list one request already returns, the two YARA sync-time methods set a timestamp no workflow reads, and the five *_aggregated walks are done here instead because the SDK's loops exit only when a page says it is the last — an appliance answering an empty page that promises another never stops them
Rich Terminal UI Formatted output with colors, tables, and panels
CLI + Library Use as command-line tool or Python package

Supported Outputs

Terminal        Rich-formatted tables, panels, and colors
Automation      JSON, YAML, table, raw
LLM pipelines   TOON (Token-Oriented Object Notation)
Code scanning   SARIF 2.1.0 (GitHub Code Scanning compatible)
Reports         JSON, TitaniumCore, PDF, HTML (dynamic analysis)

Installation

From Source

git clone https://github.com/seifreed/ReversingLabs-Client.git
cd ReversingLabs-Client
python3 -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate
pip install -e .

Development Extras

pip install -e ".[dev]"

Quick Start

Create a configuration file at ~/.rl-cli.yaml (sections live under a profile key):

default:
  titanium_cloud:
    username: "your_ticloud_username"
    password: "your_ticloud_password"

  a1000:
    host: "https://your-a1000-instance.local"
    token: "your_a1000_token_here"

Or use environment variables:

export TICLOUD_USERNAME="your_username"
export TICLOUD_PASSWORD="your_password"
export A1000_HOST="https://your-a1000.local"
export A1000_TOKEN="your_token"

The same variables can go in a .env file in the working directory instead of being exported. Either way they take precedence over the config file, so an exported A1000_TOKEN overrides the token in config.yaml while the file still supplies everything the environment leaves unset.

Then:

# Test API connections
rl-cli a1000 test
rl-cli config check-access

# Check file reputation by hash
rl-cli ticloud reputation SHA256_HASH

# Upload and analyze a file
rl-cli a1000 upload /path/to/file.exe --wait

# Get detailed report
rl-cli a1000 report SHA256_HASH

Usage

Command Line Interface

# JSON output for automation. Status messages go to stderr, so stdout is
# the document alone; a failed command exits non-zero.
rl-cli --output json a1000 report <HASH> | jq '.results[0].classification'

# Token-efficient output for LLM pipelines
rl-cli --output toon a1000 report <HASH>

# SARIF 2.1.0 log for code-scanning ingestion
rl-cli --output sarif ticloud reputation <HASH> > results.sarif

# Batch operations
rl-cli a1000 batch-reanalyze -h <HASH1> -h <HASH2>

# YARA hunting
rl-cli a1000 yara-create suspicious_strings rules.yar
rl-cli a1000 yara-matches suspicious_strings            # every match
rl-cli a1000 yara-matches suspicious_strings <HASH>     # just this sample

# Network intelligence
rl-cli a1000 domain-report <DOMAIN>
rl-cli a1000 ip-report <IP>

Available Options (Main Commands)

Command Description
rl-cli ticloud TitaniumCloud: file, URL, domain and IP reputation, AV scanners, analysis, search, sample download
rl-cli a1000 A1000: upload, reports, samples, YARA, classification, network intel
rl-cli config Configuration: show, init, save, create-profile, list-profiles, check-access

A failed command exits non-zero, with one deliberate exception: config check-access is a diagnostic, and it exits 0 whenever it managed to probe and report — including when it reports that nothing is reachable. It is the command the CLI tells you to run when a service is unreachable, so what it measured is its answer rather than its failure, and under set -e a diagnostic that exits 1 exactly when it has something to say aborts the script before you can read it. A non-zero exit from it means the check could not be made at all (an unusable config file, for instance). Branch on the measurement itself, which is on stdout:

rl-cli --output json config check-access | jq -e '.summary.services_available > 0'

Global Flags

Option Description
--config <file>, -c Path to configuration file
--profile <name>, -p Configuration profile to use
--output <fmt>, -o Output format: rich, json, yaml, table, raw, toon, sarif
--quiet, -q Suppress progress output; warnings and errors remain
--verbose, -v Verbose output
--version Print the version and exit

TitaniumCloud Commands

Command Description
reputation Malware presence and classification for one hash, or a batch in one request
analysis Full analysis record for a hash
av-scanners Per-engine AV scanner results for a hash
analyze-url Reputation for a URL
search Advanced Search over the TitaniumCloud corpus
upload Submit a file for TitaniumCloud analysis
download Write the stored sample to disk (live malware)
download-status Whether TitaniumCloud holds the sample bytes for a hash
domain-report Threat intelligence for a domain
domain-files Files downloaded from a domain
domain-urls URLs seen on a domain
domain-ips Domain-to-IP resolutions
domain-related Domains sharing a top parent domain
ip-report Threat intelligence for an IP address
ip-files Files downloaded from an IP address
ip-urls URLs seen on an IP address
ip-domains IP-to-domain resolutions
url-files Files downloaded from a URL
uri-index SHA-1s of every sample seen at a URI, domain, IPv4 or email address

reputation takes several hashes — as arguments, repeated -h, or -f with one per line — and grades them in a single bulk request instead of one metered request per hash. A batch must be all of one hash type, which is what the bulk endpoint queries by. A hash named twice is looked up once, and one hash or five hundred are reported the same way: one graded record per sample, in every output format.

The nine lookups from domain-files down return the first page of results. The --all flag pages through every result, which on a busy address is the whole corpus at 1000 records a metered page; --max-results N stops after N results and pages on its own, so --all need not be given as well. A walk that comes back exactly N results long says that more may be waiting, since what ended it may have been the cap rather than the corpus.

download writes live malware: the file lands owner-only (0600), a symlink at the destination is refused rather than followed, and an interrupted download leaves nothing behind.

# Bulk triage: one request, not 500
rl-cli --output json ticloud reputation -f hashes.txt

# Pivot from a C2 address
rl-cli ticloud ip-report <IP>
rl-cli ticloud ip-files <IP> --max-results 500
rl-cli ticloud domain-related <DOMAIN>

# Fetch the sample itself
rl-cli ticloud download-status <HASH>
rl-cli ticloud download <HASH> --output-dir ./samples

A1000 Command Groups (52 Commands)

Group Commands
File operations upload, upload-and-analyze, status, report, summary-report, titanium-report
Sample management list, search, download, delete, reanalyze, batch-delete, batch-reanalyze
Extracted files extracted, containers
Classification & tags set-classification, get-classification, delete-classification, add-tags, get-tags, remove-tags
YARA rulesets yara-list, yara-create, yara-content, yara-delete, yara-toggle, yara-matches, yara-publish, yara-update-now, yara-update-interval
YARA retro hunts yara-cloud-retro, yara-cloud-retro-status, yara-local-retro, yara-local-retro-status
YARA repositories yara-repo-list, yara-repo-create, yara-repo-update, yara-repo-delete
Network intelligence domain-report, ip-report, ip-files, ip-domains, ip-urls, network-url-report
URL analysis submit-url, url-report, url-status
Reports dynamic-report-create, dynamic-report-status, dynamic-report, report --format pdf
Connection test, config-dump

ip-files, ip-domains and ip-urls return the first page of results; add --all to page through every result. extracted lists every extracted file in one request, so it takes no paging flag; its -a/--download-all writes the extracted files to disk.

Eight commands ask before they act, because what they do cannot be undone: delete, batch-delete, delete-classification, remove-tags with no -t, yara-delete, yara-cloud-retro -o clear, yara-repo-delete, and config create-profile, which overwrites a stored profile's credentials. Each takes --yes, which takes the confirmation as given — a script or a cron job has no answer to type, and an unanswered prompt aborts and exits 1 having done nothing. The prompt is still the default, so a bare invocation at a terminal asks as it always did.

# Scripted removal: nothing on stdin to answer with
rl-cli a1000 yara-repo-delete 7 --yes
rl-cli a1000 batch-delete -f hashes.txt --yes

yara-repo-create and yara-repo-update take the credential the appliance uses to clone a private rule repository — a GitHub PAT, typically. Pass --api-token-stdin instead of --api-token <token>: an argument is readable in ps output by every other user on the machine for as long as the call runs, and the line stays in your shell history file afterwards, which is why the bare flag is discouraged. With --api-token-stdin the token is piped in, or typed at a hidden prompt when stdin is a terminal. Exactly one trailing newline is stripped and nothing else, so a piped token arrives as written. Supplying both ways at once is a usage error rather than a precedence rule. The flag itself still works unchanged — a public repository is stated as an empty --api-token, or by passing neither to yara-repo-create.

# Interactive: nothing is echoed, nothing is left in the history file
rl-cli a1000 yara-repo-create --url <REPO_URL> --name org-rules --api-token-stdin

# Scripted: the token comes from a secret store, never from argv
pass show github/rules-pat | rl-cli a1000 yara-repo-create --url <REPO_URL> --name org-rules --api-token-stdin

Python Library

Basic Usage

Each A1000 service covers one area of the appliance, and an A1000Session is one connection to it: build the services you need from a single session and they authenticate once between them, however many areas you touch. The TitaniumCloud side splits the same way, without a session to share — there is no connection to share, since each call builds its own API handle — so enriching an address takes TitaniumCloudNetworkService and the file endpoints take TitaniumCloudService.

from rl_cli.services import (
    A1000ReportService,
    A1000SampleService,
    A1000Session,
    TitaniumCloudNetworkService,
    TitaniumCloudService,
    upload_and_get_report,
)
from rl_cli.config import get_settings
from pathlib import Path

# `get_settings()`, not `Settings()`: the constructor only holds the values it
# is handed, so a bare `Settings()` talks to ReversingLabs' public clouds
# whatever your config file says. This is the discovery the CLI itself does.
settings = get_settings()
ticloud = TitaniumCloudService(settings)

# Check file reputation
reputation = ticloud.get_file_reputation("SHA256_HASH")

# Enriching an address needs the network half and nothing else. Every pivot
# comes in two: the plain call answers the first page, the `_aggregated` one
# walks every page and takes a `max_results` budget.
network = TitaniumCloudNetworkService(settings)
resolutions = network.get_domains_from_ip("8.8.8.8")

# One session, one connection: both services below share it.
session = A1000Session(settings)
samples = session.service(A1000SampleService)
reports = session.service(A1000ReportService)

# Upload and analyze a file. `task_id` is the digest A1000 returned for the
# submission — its SHA256 when the response carries one, otherwise the SHA1 —
# so the same value polls the analysis and fetches the finished report. It is
# absent when the upload failed (`None`) and when the appliance took the file
# but its answer could not be read, so ask for it rather than indexing.
result = samples.upload_file(Path("/path/to/file.exe"), comment="Automated analysis")
task_id = (result or {}).get("task_id")
if task_id and samples.wait_for_analysis(task_id, timeout=300):
    report = reports.get_report(task_id)

# Or as one step: the workflow spans both areas, so it takes both services.
summary = upload_and_get_report(samples, reports, Path("/path/to/file.exe"), "summary")

YARA Hunting

from rl_cli.services import A1000YaraService

# A third area of the appliance, still over the connection opened above.
yara = session.service(A1000YaraService)
sample_hashes = [task_id] if task_id else []

# get_yara_matches returns the list of matching samples (empty when none),
# or None if the call failed.
matches = {
    h: entries
    for h in sample_hashes
    if (entries := yara.get_yara_matches("suspicious_strings", h))
}

# Ends the connection for every service built from the session.
session.close()

Configuration

Configuration sources in priority order: CLI options → environment variables → config file → defaults.

Config file locations searched (in order): ./config.yaml, ./.rl-cli.yaml, ~/.config/rl-cli/config.yaml, ~/.rl-cli.yaml.

See config.example.yaml and .env.example for the complete reference, including proxy support, SSL verification, and output settings.


Security

  • Never commit credentials — use config files (chmod 600) or environment variables
  • Enable SSL verification in production; prefer API tokens over username/password
  • Never pass the YARA repository token as --api-token <token>: argv is world-readable in ps and the line is kept in your shell history. yara-repo-create and yara-repo-update take --api-token-stdin, which pipes it in or prompts for it without echoing. It is deliberately not read from the environment or .env — that file is plaintext in the working directory, and nothing here creates it 0600 or keeps it out of a commit
  • The commands that delete something prompt first, and --yes is the only thing that skips the prompt: neither --quiet nor a closed stdin is taken for a yes, so a non-interactive run has to say that it means it
  • download and extracted --download-all print a warning naming the destination before writing anything. No classification is consulted: every file A1000 hands back is treated as live malware. --output-dir defaults to the current directory, so point it at an isolated directory or sandbox
  • Codebase scanned with Bandit and pip-audit, linted with Ruff

Requirements

  • Python 3.13 or 3.14
  • ReversingLabs API credentials (TitaniumCloud and/or A1000)
  • See pyproject.toml for dependencies and extras. Every runtime dependency is declared with both a floor and a major-version ceiling, so a fresh install resolves the same majors this project is tested against
  • The package ships a PEP 561 py.typed marker, so when it is imported as a library its annotations are type information your own mypy will read, rather than Any

Contributing

Contributions are welcome. CONTRIBUTING.md has the gate commands, the places adding a command or an output format touches, and where the conventions are written down.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Enable the pre-push hook once per clone — it is not on by default, so on a fresh clone nothing checks a push until CI does:

git config core.hooksPath .githooks

It runs pre-push-check.sh, which scans for credentials and malware samples and then runs the same quality gate CI runs. To run that gate by hand:

ruff check rl_cli/ tests/
ruff format --check rl_cli/ tests/
mypy rl_cli/ tests/
bandit -r rl_cli/ -q
pip-audit --skip-editable
pytest -q

pytest -q enforces the 95% line-and-branch coverage floor on its own: the flags live in pyproject.toml, so no spelling of the whole-suite command skips it. A narrower run — one file, a nodeid, -k, -m, --deselect — still prints the coverage report but is not held to the floor, because 95% of rl_cli was never that run's claim.


Support the Project

If this project is useful in your workflows, you can support development:

Buy Me A Coffee

Acknowledgments


License

This project is licensed under the MIT license. See LICENSE.

Attribution


Built for practical malware analysis and threat intelligence automation

Metadata

Release files for reversinglabs-cli 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for reversinglabs-cli 0.1.0
File Size Uploaded
reversinglabs_cli-0.1.0.tar.gz 467.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for reversinglabs-cli 0.1.0
File Interpreter ABI Platform
reversinglabs_cli-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 681.8 kB

Release files / reversinglabs_cli-0.1.0.tar.gz

Download URL reversinglabs_cli-0.1.0.tar.gz
Size 467.2 kB
Tags Source
SHA-256 checksum
How to use checksums
0e5d898f768f8fdbb243108e1648d55c100ab5e4d7c446f6dfa7ecd63628c398
BLAKE2b-256 checksum
How to use checksums
c17bdb4545701e90c6f8f25bb8b406df9a7a9abc048ac21cdbb2a1c1bba2a6b8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 24, 2026.

Transparency log

Release files / reversinglabs_cli-0.1.0-py3-none-any.whl

Download URL reversinglabs_cli-0.1.0-py3-none-any.whl
Size 214.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b16ed0d0a311628b0ca40932884ccef7fbcec4c6adf1ff8d19de00ec5fc6914f
BLAKE2b-256 checksum
How to use checksums
2cf5d8e59511864ab0cd1ea4ca4769ec9747fdb29cea721a05dc30c82ea5c398
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 24, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page