# Python dependencies for the cxg pentest orchestrator and its test suite.
#
# Until this file existed there was no dependency manifest anywhere in the
# repository, which meant the 224-test Python suite could not run in CI at all:
# .github/workflows/ci.yml had five jobs and every one of them was `cargo`.
# The suite was therefore green only on whatever happened to be installed on a
# developer's machine, and a change that broke it could merge unnoticed — for a
# tool whose entire job is deciding whether an application is safe.
#
# Versions are pinned exactly, matching this repository's existing convention of
# committing Cargo.lock: a security scanner's own behaviour should not change
# because a transitive release shipped. To bump one, change it here, run
# `make test-pentest`, and commit the result.
#
# pytest-asyncio is not optional: pentest/pytest.ini sets `asyncio_mode = auto`,
# so without it every async test errors out at collection rather than skipping.
pytest==9.1.1
pytest-asyncio==1.4.0

# Drives Chromium for the web substrate and attaches to Electron over CDP for
# the desktop substrate. Only the Python package is needed to run this suite —
# `playwright install` is not, because the tests that would drive a real browser
# or a real Electron app gate themselves on the fixture app's node_modules being
# present and skip when it is not.
playwright==1.60.0

# Registers and decrypts OAST canaries in pentest/oast.py, which is what turns
# --oast from a URL builder into actual blind-vuln confirmation. The interactsh
# protocol wraps a per-response AES-256 key with RSA-OAEP(SHA-256) and encrypts
# each interaction as IV || AES stream; the standard library has neither RSA nor
# AES, so there are exactly three ways to read a callback back:
#
#   1. Hand-roll AES inside a security scanner — indefensible to save an install.
#   2. Shell out to ProjectDiscovery's interactsh-client binary — pip cannot
#      install it, so CI and every operator machine needs an out-of-band Go
#      install, and when it is absent (or its encryption mode disagrees with the
#      server, as in projectdiscovery/interactsh#1333) the observable result is
#      an empty output file. That is indistinguishable from "the target never
#      called back", and a scanner that reads absence as refutation is exactly
#      the defect oast.py was written to fix.
#   3. This: one exactly-pinned wheel.
#
# The pin is what the header above asks for — the objection is to behaviour
# changing when a transitive release ships, which an exact version answers — and
# the marginal cost is smaller than playwright, already pinned above, which pulls
# whole browsers. oast.py imports it lazily inside a function, so a machine where
# the wheel is unavailable degrades to OastSession.pollable == False (canary
# injection still works, readback does not) instead of failing to import and
# taking down every scan.
cryptography==50.0.0

# Native gRPC (cxg.grpc). gRPC-Web needs nothing — it is an ordinary HTTP POST
# that cxg.raw already sends — but a BARE gRPC service speaks HTTP/2 with binary
# framing and carries its verdict in a trailer, and there are only two ways to
# reach one:
#
#   1. Hand-roll HTTP/2 on top of cxg.socket — HPACK, SETTINGS, flow control and
#      trailer parsing, written by hand inside a security scanner, so that every
#      authorization verdict it produces rests on our own framing being correct.
#   2. This: the reference implementation, exactly pinned.
#
# grpcio is a compiled wheel, which is heavier than anything else here. Verified
# before pinning: cp312 and cp313 manylinux wheels exist for the CI matrix, and
# a cp314 wheel exists for macOS, so no operator or CI job needs a compiler.
#
# grpc_client.py imports it LAZILY, inside a function, for the same reason
# oast.py imports cryptography that way: a machine where the wheel is
# unavailable must degrade to "the grpc capability is not offered" — templates
# skipped, never refuted — instead of failing to import and taking down every
# scan, including the HTTP ones that never needed it.
#
# protobuf is grpcio's companion for descriptor-driven message building. It is
# pinned rather than left to grpcio's own resolution because a transitive
# protobuf release changing how a descriptor pool loads would change which
# messages cxg can build, and this file's whole convention is that a security
# scanner's behaviour does not move because someone else shipped.
grpcio==1.83.1
protobuf==7.36.1

# Server reflection — how cxg asks a gRPC service to describe its own methods
# when the operator has no protoc output to hand. It is a SEPARATE, tiny,
# pure-Python wheel from grpcio, and it is listed separately because the two can
# genuinely differ: a machine can speak gRPC perfectly well and still be unable
# to ask a server what its methods are, in which case --grpc-descriptor-set
# still works and only reflection is withheld.
#
# The alternative was hand-encoding the reflection protocol's own messages. The
# request is trivial, but decoding the response means parsing nested protobuf
# with a oneof by hand — inside a security scanner, so that every typed probe
# rests on our own parser being right. This is the reference implementation of
# exactly those messages, version-locked to the grpcio above.
#
# Imported lazily like grpcio, so its absence degrades to "reflection is not
# available" rather than breaking the scan.
grpcio-reflection==1.83.1

# Reads the operator's scope.yaml (`--scope-file`), which is the control that
# says what a scan is allowed to touch. It is listed here because the flag is a
# containment bound, not a convenience: `ScopeConfig.load` REFUSES the run when
# PyYAML is absent rather than scanning on defaults, so without this wheel
# `--scope-file` does not degrade — it stops working entirely.
#
# `cxg pentest install` does not install this file; it CHECKS for playwright
# and yaml and prints a pip line. Optional AI SDKs are checked by their provider. So an operator who skipped or
# ignored that step can still reach an engagement without PyYAML, which is
# exactly why the load path refuses instead of warning. Pinned
# for the same reason as everything else here: a security scanner's behaviour
# should not move because someone else shipped a release.
#
# scope.py imports it at module scope inside a try/except ImportError, so its
# absence degrades to "a scope file cannot be read" (a refusal naming the
# dependency) rather than breaking every import of the orchestrator.
PyYAML==6.0.3

# The single-packet race transport (pentest/race_transport.py) for cxg.race.
# A true TOCTOU race must deliver its requests with no inter-request gap a lock
# could close in, and the way to do that is one HTTP/2 connection with every
# HEADERS frame sent first and every stream's terminating frame flushed
# together — which needs frame-level control no HTTP client library exposes.
# There are two ways to get it:
#
#   1. Hand-roll HTTP/2 framing — HPACK, SETTINGS, flow control — by hand inside
#      a security scanner, so that every race verdict rests on our own framing
#      being correct.
#   2. This: the reference sans-I/O HTTP/2 implementation, exactly pinned. It is
#      pure Python and tiny (httpx already pulls it in for HTTP/2 support).
#
# race_transport imports it LAZILY inside a function and race_transport.fire
# checks h2_available() BEFORE offering ALPN h2, so a machine without the wheel
# degrades to the HTTP/1.1 last-byte-synchronisation fallback rather than
# failing to import — the same contract oast.py and grpc_client.py follow.
h2==4.3.0
