Skip to content

Add virustotal command #288

@xransum

Description

@xransum

Overview

This is a blank-slate feature request for the virustotal integration. No files have been added yet - the contributor is expected to build everything from scratch across two files: the API client layer in vt.py and the CLI command layer in virustotal.py.

The sections below describe exactly what needs to be built and the project's compliance requirements that must be met before requesting a review.


File structure

This project splits every feature domain into two files. Follow the same pattern:

File Responsibility
src/valkyrie_tools/vt.py VirusTotalClient dataclass, _get_api_key helper, VT_NO_API_KEY_MESSAGE constant. No Click imports.
src/valkyrie_tools/virustotal.py Click CLI commands only. Imports from vt.py.

Tests follow the same split:

File Responsibility
tests/test_vt.py Unit tests for VirusTotalClient and helpers (mocked requests)
tests/test_virustotal.py CLI tests via CliRunner, inheriting BaseCommandTest

Register the entry point in pyproject.toml:

virustotal = "valkyrie_tools.virustotal:cli"

What to build

Commands

Command What it does VT API reference
virustotal url "<url>" Submit a URL for scanning POST /urls
virustotal domain "<domain>" Retrieve a domain report GET /domains/{domain}
virustotal ip "<ip>" Retrieve an IP address report GET /ip_addresses/{ip}
virustotal hash "<hash>" Retrieve a file report by MD5/SHA-1/SHA-256 GET /files/{hash}
virustotal file <path> Upload a local file for scanning POST /files

Each command accepts a -j / --json flag. Without it, print a concise human-readable summary of the most relevant response fields (e.g. last_analysis_stats, reputation). With it, print the full JSON payload via json.dumps(result, indent=2) for piping.

The API key is stored in the package config and read at runtime:

valkyrie config set virustotalApiKey <your-key>

If the key is not set, the command must exit with a non-zero code and print VT_NO_API_KEY_MESSAGE (define it in vt.py).


Third-party client evaluation

Before writing anything, check whether a reputable VirusTotal Python client exists that is compatible with >=3.8,<4.0 and matches this project's synchronous requests-based HTTP pattern.

The official client vt-py must not be used. It requires aiohttp and aiofiles (async-only design) which is incompatible with every other HTTP call in this project. If you find another library that fits, document your reasoning in this issue.

If no suitable third-party library exists (the expected outcome), implement the following internal wrapper in src/valkyrie_tools/vt.py:

@dataclass
class VirusTotalClient:
    api_key: str
    base_url: str = "https://www.virustotal.com"
    api_version: str = "v3"
    base_api_url: str = field(init=False)

    def __post_init__(self) -> None:
        self.base_api_url = f"{self.base_url}/api/{self.api_version}"

    def _build_url(self, path: str) -> str:
        return f"{self.base_api_url}/{path.lstrip('/')}"

    # Methods to implement (names flexible):
    # scan_url(url: str) -> Dict[str, Any]
    # get_domain(domain: str) -> Dict[str, Any]
    # get_ip(ip: str) -> Dict[str, Any]
    # get_file(hash_value: str) -> Dict[str, Any]
    # scan_file(path: str) -> Dict[str, Any]

Each method should use requests (already a project dependency) with headers={"x-apikey": self.api_key} and call response.raise_for_status() before returning response.json().


Implementation checklist

Work through these in order. Every item must be complete before requesting a review.

src/valkyrie_tools/vt.py - API layer

  • Evaluate third-party VT libraries; document decision in PR
  • Define VT_NO_API_KEY_MESSAGE constant
  • Implement _get_api_key helper
  • Implement VirusTotalClient.__post_init__ and _build_url
  • Implement VirusTotalClient.scan_url
  • Implement VirusTotalClient.get_domain
  • Implement VirusTotalClient.get_ip
  • Implement VirusTotalClient.get_file
  • Implement VirusTotalClient.scan_file

src/valkyrie_tools/virustotal.py - CLI layer

  • Implement _print_result helper (summary view + --json path)
  • Implement scan_url_cmd CLI sub-command body
  • Implement get_domain_cmd CLI sub-command body
  • Implement get_ip_cmd CLI sub-command body
  • Implement get_hash_cmd CLI sub-command body
  • Implement scan_file_cmd CLI sub-command body

Tests

  • Write unit tests in tests/test_vt.py (mock requests, cover all VirusTotalClient methods and helpers)
  • Write CLI tests in tests/test_virustotal.py (use CliRunner, inherit BaseCommandTest)

Final

  • Register virustotal = "valkyrie_tools.virustotal:cli" in pyproject.toml
  • Add virustotal CLI and API entries to docs/reference.rst
  • All quality gates pass (see below)

Compliance requirements

This project has strict quality gates that CI will reject if not met. Read DEVELOPMENT.md before writing any code. The key requirements are summarised below.

Formatting and linting

black (80-char line length), isort, flake8, and darglint all run automatically. Run before every commit:

poetry run nox -s pre-commit

Or manually:

poetry run black src tests
poetry run isort src tests
poetry run flake8 src tests

Key rules:

  • Maximum line length: 80 characters
  • Import order: black-compatible (isort profile = black)
  • Cyclomatic complexity: max 10
  • No unused imports, no bare except, no print (use click.echo)

See DEVELOPMENT.md - Code Style and DEVELOPMENT.md - Linting.

Docstrings

Every public function, class, and module must have a Google-style docstring. darglint enforces that Args:, Returns:, and Raises: sections are present whenever the function has arguments, a return value, or raises exceptions.

Example of a compliant docstring:

def get_domain(self, domain: str) -> Dict[str, Any]:
    """Retrieve a domain report from VirusTotal.

    Args:
        domain: The domain name to look up (e.g. ``"example.com"``).

    Returns:
        Parsed JSON response from the VirusTotal API.

    Raises:
        requests.HTTPError: If the API returns a 4xx or 5xx status.
    """

See DEVELOPMENT.md - Docstring Standards.

Type annotations

mypy runs in strict mode. Every function argument and return type must be fully annotated. requests stubs (types-requests) are already a project dependency. Do not use # type: ignore without a comment explaining why.

Run manually:

poetry run mypy src tests docs/conf.py

See DEVELOPMENT.md - Type Annotations.

Test coverage

Coverage must be 100%. Every branch of every new function must be exercised. Never make real network calls in tests - patch VirusTotalClient methods and requests calls with unittest.mock.patch.

After writing tests:

poetry run coverage run -m pytest
poetry run coverage report

Any line shown as missing in the report must be covered before the PR can merge.

See DEVELOPMENT.md - Tests.

Auto-generated documentation

Add the virustotal CLI and API entries to docs/reference.rst, then verify the Sphinx build produces zero warnings:

poetry run nox -s docs-build

If you add or rename any public function or class, update the docstring and confirm the build still passes.

See DEVELOPMENT.md - Session reference (docs-build).

Full quality gate

Run the complete suite before pushing:

poetry run nox

This runs pre-commit, safety, tests, xdoctest, and docs-build in sequence. All sessions must be green.

Version

Bump the version from 1.3.2 to 1.4.0 in pyproject.toml (minor bump for a new backward-compatible feature).


Getting started

# 1. Fork or clone the repo
gh repo fork xransum/valkyrie-tools --clone

# 2. Install dependencies
poetry install

# 3. Check out this branch
git checkout feature/virustotal

# 4. Install pre-commit hooks (run once)
poetry run pre-commit install

# 5. Iterate - run the relevant session after each change
poetry run nox -s tests       # after logic changes
poetry run nox -s pre-commit  # before committing
poetry run mypy src tests docs/conf.py  # after type annotation changes

# 6. Full gate before pushing
poetry run nox

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions