Documentation

Outbound connections

Every host PYXA calls, the guard that records each call, and how to refuse anything not configured.

PYXA calls out to scanners, cloud APIs, threat intelligence feeds (CISA KEV, VulnCheck, EPSS, Microsoft's update guide, CERT advisories, exploit catalogues), AI providers, ticketing systems, webhooks and the identity provider. Every one of those HTTP calls passes one check, core/egress.py. This page covers what that check does, how to run it, and what it cannot see.

What the guard does

PYXA wraps requests.Session.send once, at start-up (core/apps.py). Every connector, feed client and provider in the codebase sends through it, including the module-level helpers (requests.get() builds a Session) and each hop of a redirect. Before a request leaves, the guard:

  1. Matches the destination host against the approved list:
    • built-in feeds: www.cisa.gov, api.first.org, api.vulncheck.com, api.msrc.microsoft.com, www.cert.ssi.gouv.fr, wid.cert-bund.de, the Exploit-DB, Metasploit and PoC-in-GitHub sources, and the Anthropic and OpenAI APIs;
    • hosts named by configuration: every URL in a connector's settings, a cloud connector's default API host (only for a connector of that type), ticketing base URLs, AI provider URLs, webhook targets, the OIDC issuer and endpoints, each CSAF provider added under Threat intel > CERT advisories, and the distribution hosts that provider's metadata names (recorded when it is read, so documents served from another host than the metadata are admitted on the provider's word);
    • hosts an administrator approved, on the page or with PYXA_EGRESS_EXTRA_HOSTS.
  2. Applies the mode:
    • Observe (the default): the call is sent and recorded. A host outside the list is flagged on the page and logged as a warning.
    • Enforce: a call to a host outside the list is refused before a socket opens, and so is a redirect that leads to one. The caller receives a ConnectionError naming the host and where to approve it, so a connector's sync fails with that message instead of a timeout.
  3. Records the call against its host: calls, server errors and failed connections, refusals, calls sent with certificate verification off, bytes received, last status, last method and the PYXA module that made it.
  4. Logs one JSON line on the core.egress logger (see below).

Turning on enforce mode

  1. Run in observe mode for at least one full cycle of every schedule: daily feeds, each connector's sync, the monthly Patch Tuesday reads.
  2. Open Administration > Outbound connections. Every row marked "Not configured" is a host PYXA called that no configuration names. Approve the ones that are expected, such as a proxy or a regional API host. Find out why the others were called before going further.
  3. Switch the mode to Enforce. The worker picks up the change within 30 seconds.
  4. To keep the setting out of reach of the administration UI, fix it in the deployment:
   # .env
   PYXA_EGRESS_MODE=enforce
   PYXA_EGRESS_EXTRA_HOSTS=proxy.example.com,*.vendor.example

With PYXA_EGRESS_MODE set, the page shows the mode and refuses to change it, so a compromised administrator account cannot switch back to observe.

Every change to the policy and every approval is written to the audit log (egress_policy_change, egress_destination_approve, egress_destination_revoke).

Is a destination compromised?

Most of these checks need no configuration.

RiskWhat PYXA doesWhere to see it
A call to a host nobody configured (a tampered dependency, an injected URL)Flagged in observe mode; refused in enforce modeOutbound connections, row state; core.egress warning
A feed served from somewhere else after a redirectEach redirect hop is checked like a new callRow of the redirect target
TLS interception, a swapped certificate, a redirected DNS nameDaily certificate check: SHA-256 of the leaf certificate, issuer and expiry, with the previous fingerprint kept when it changesRow state "Certificate changed"; egress_certificate_changed log event
Certificate verification switched off for a connectorEach such call is countedRow state "N calls without certificate check"
A truncated, reformatted or tampered KEV catalogueA read with more than 5% fewer CVEs than the last applied one is held, not appliedFeed integrity row; the sync fails with the counts
A connector URL pointed somewhere newThe new host is approved because configuration names it; the change is in the audit logAudit log; the destination's first-seen date

The certificate check runs as the maintenance job Check outbound certificates (enable it in Administration > Maintenance), or on demand with Check certificates on the page. It connects through the same HTTPS proxy and CA bundle that requests uses (HTTPS_PROXY, NO_PROXY, REQUESTS_CA_BUNDLE), so it sees the certificate PYXA's own calls are shown. Behind an intercepting proxy, that certificate is the proxy's, and its issuer is shown on the row.

A KEV read that was held is applied with:

docker compose exec django python manage.py sync_cisa_kev --accept-shrink

Sending the log to a SIEM

Each call is one line on the core.egress logger at INFO. With LOG_FORMAT=json the whole record is JSON; the message itself always is:

{"event":"egress","method":"GET","host":"api.vulncheck.com","path":"/v3/index/vulncheck-kev",
 "caller":"core.threat_intel.vulncheck:_get_page","approved":true,"mode":"enforce",
 "tls_verified":true,"outcome":"sent","status":200,"bytes":48211,"ms":412}
FieldMeaning
outcomesent, refused (enforce mode) or error (no response; error names the exception)
approvedThe host is on the approved list
callerThe PYXA module and function that made the call
pathThe URL path. The query string is never logged, because it can carry a key
tls_verifiedHTTPS with certificate verification on

Other events: egress_certificate_changed (host, previous and current fingerprint) and egress_feed_held (feed, entries, baseline).

Suggested alerts: any outcome=refused; approved=false in observe mode; any egress_certificate_changed outside a known renewal; any egress_feed_held.

Limits

  • SMTP, LDAP and the OpenVAS GMP socket are not covered. They do not use requests. Their destinations are fixed by their own settings: the mail server, the directory server and the GMP host.
  • The guard runs inside PYXA's process. It stops a call PYXA's own code makes to the wrong place. It cannot stop code that has already taken over the process, which could open a socket directly. Pair it with an egress rule on the firewall or proxy that allows only the hosts on this page. Export the list from Configured destinations on the page.
  • A configured host is trusted as configured. If someone changes a connector's URL, the new host is approved along with the change. The audit log records who made the change, and the destination's first-seen date shows when the new host was first called.
  • The feed check is about size, not content. A catalogue that is the usual size but wrong is applied. Only the CISA KEV sync is checked today.
  • Counters start when this version starts. Calls made before the upgrade are not on the page.

Files

FileRole
core/egress.pyPolicy, the Session.send hook, audit, certificate check, feed shrink guard
core/models_egress.pyEgressSettings, EgressDestination, FeedBaseline
core/views_egress.pyThe Administration page and its API
core/management/commands/sync_cisa_kev.pyApplies the shrink guard; --accept-shrink
core/tasks.py check_egress_certificatesDaily certificate check

Generated from docs/outbound-connections.md in the PYXA repository.

Make security everyone's job, starting this week.

Run the free edition on your own server today, or see PYXA on your own scanner data in a 30-minute demo.