Documentation

Sending files

Push scan exports to PYXA from a pipeline, a scheduled job or by hand, with a per-connector upload key.

A file connector (CSV/JSON, SARIF, ASFF, EASM, patch deployment, bug bounty, penetration test, SharpHound) reads an export rather than calling a tool's API. This page covers getting that export to PYXA without anyone copying it by hand: from a CI pipeline, a scheduled job or a script.

Which way to send files

Push over HTTPS (this page)Shared folder
What the sender needsthe PYXA address and one upload keywrite access to a share mounted at the import folder
What it can reachone connector's upload, nothing elseevery file in the import folder
When the import runsas soon as the file arrivesat the connector's next schedule
What the sender learnsthe import's own summary, and an exit codenothing; someone reads the connector page

Push is the better default. The key is the smallest permission that works: it cannot read a finding, call the API or reach another connector. The sender also finds out whether the import worked.

A CI/CD pipeline is not a third option. It is a place to run the push from, so the scan that produced the file sends it in the same job. Keep the shared folder for a tool that can only write files to a share.

Both ways read the same formats, which are listed per connector in connectors.md.

1. Make the connector's upload key

Open the connector in PYXA (Administration, Integrations, then the connector) and use Make upload key in its Push files panel. Only an administrator sees that panel.

  • The key starts pxu_ and appears once. PYXA stores only its hash, so if the key is lost, make a new one.
  • Replace key makes a new one; the previous key stops working at once.
  • Revoke key stops it with no replacement.
  • The key belongs to the administrator who made it. If that account is deactivated, the key stops working with it.
  • The panel shows the connector's id, the address files go to, the size limit, and the last five uploads with what each one imported. When a key is made it also shows the curl and pyxa commands, ready to copy.

On the server, pyxa keys make <connector id> does the same thing (see below).

Store the key where your pipeline keeps secrets (a GitHub Actions secret, a masked GitLab CI variable, a vault). Do not commit it, and do not pass it with --key on a shared machine, where other users can read the process list; use the PYXA_UPLOAD_KEY variable instead.

2. Send a file with pyxa

pyxa is one Python file with nothing to install beyond Python 3.9 or later. Each PYXA serves its own copy, so the client always matches the server, and a runner with no internet access can still fetch it:

curl -fsS https://pyxa.example.com/downloads/pyxa.py -o pyxa.py

Settings come from the environment, or from options with the same names:

VariableOptionWhat it is
PYXA_URL--urlPYXA's address, e.g. https://pyxa.example.com
PYXA_CONNECTOR--connectorthe connector's id, shown on its page
PYXA_UPLOAD_KEY--keythe connector's upload key
PYXA_CA_CERT--ca-certPYXA's certificate, when PYXA signs its own (below)
export PYXA_URL=https://pyxa.example.com
export PYXA_CONNECTOR=7
export PYXA_UPLOAD_KEY=pxu_...            # from your secret store

python3 pyxa.py push trivy.sarif --wait
trivy.sarif received for Trivy repo. The import is queued.
Upload 42: trivy.sarif to Trivy repo, done
Assets: 1 created/updated
Findings: 12 created
Imported 12 SARIF findings from file.

Without --wait, push returns once the file is stored. pyxa status 42 reads what became of it, and pyxa status 42 --wait waits for it. --timeout sets how long --wait waits, in seconds; the default is 900.

A CSV import holds three files. --kind says which one this is: vulns (the default), assets or kb.

python3 pyxa.py push assets.csv --kind assets --wait
python3 pyxa.py push vulns.csv --kind vulns --wait

Exit codes, for a pipeline to act on:

CodeMeaning
0imported (or, without --wait, received)
1the import ran and failed; the summary says why
2refused before anything was stored, or a setting is missing; the message says which
3still importing when --wait ran out

Send a file with curl or PowerShell

The client is a convenience. The API is two calls.

curl -fsS -X POST "$PYXA_URL/api/v1/connectors/$PYXA_CONNECTOR/upload/" \
     -H "Authorization: Bearer $PYXA_UPLOAD_KEY" \
     -F "file=@trivy.sarif"
# a CSV import also takes:  -F "kind=assets"

The answer is 202 with a receipt:

{
  "upload_id": 42,
  "connector": "Trivy repo",
  "file": "trivy.sarif",
  "size": 18234,
  "sha256": "660bf1b5...",
  "state": "received",
  "status_url": "https://pyxa.example.com/api/v1/connectors/7/uploads/42/",
  "message": "trivy.sarif received for Trivy repo. The import is queued."
}

Poll status_url with the same key until state is done or failed; summary then holds the import's own account of what it did.

curl -fsS -H "Authorization: Bearer $PYXA_UPLOAD_KEY" "$PYXA_URL/api/v1/connectors/$PYXA_CONNECTOR/uploads/42/"

PowerShell 7 on a Windows server, for example after a WSUS export:

$headers = @{ Authorization = "Bearer $env:PYXA_UPLOAD_KEY" }
$receipt = Invoke-RestMethod -Method Post -Headers $headers `
  -Uri "$env:PYXA_URL/api/v1/connectors/$env:PYXA_CONNECTOR/upload/" `
  -Form @{ file = Get-Item "C:\exports\wsus-updates.csv" }
$receipt.message

Windows PowerShell 5.1 has no -Form. Use curl.exe, which Windows 10 and later include, with the arguments above.

PYXA with its own certificate

Out of the box PYXA serves HTTPS with a certificate it signs itself, which no runner trusts. Download it from Administration, HTTPS, Download the current certificate, and give it to the sender:

python3 pyxa.py push trivy.sarif --ca-cert pyxa.crt     # or PYXA_CA_CERT=pyxa.crt
curl --cacert pyxa.crt ...

When PYXA renews its certificate, download it again. A certificate from your own authority that the runner already trusts needs nothing. pyxa --insecure skips the check entirely; it is for a first test, never for a pipeline, because anyone in the path could then collect the key.

GitHub Actions

The runner must reach PYXA. A GitHub-hosted runner reaches only an address on the internet; for a PYXA on an internal network, use a self-hosted runner inside it.

Secrets and variables, under the repository's Settings:

  • secret PYXA_UPLOAD_KEY, the connector's upload key
  • secret PYXA_CA_CERT_PEM, the certificate's contents, only if PYXA signs its own
  • variables PYXA_URL and PYXA_CONNECTOR
# .github/workflows/pyxa.yml
name: Scan and send to PYXA
on:
  push:
    branches: [main]
  schedule:
    - cron: "17 3 * * *"

jobs:
  scan:
    runs-on: ubuntu-latest      # or [self-hosted] for a PYXA on an internal network
    env:
      PYXA_URL: ${{ vars.PYXA_URL }}
      PYXA_CONNECTOR: ${{ vars.PYXA_CONNECTOR }}
      PYXA_UPLOAD_KEY: ${{ secrets.PYXA_UPLOAD_KEY }}
      PYXA_CA_CERT_PEM: ${{ secrets.PYXA_CA_CERT_PEM }}
    steps:
      - uses: actions/checkout@v4

      - name: Scan with Trivy
        uses: aquasecurity/trivy-action@0.28.0
        with:
          scan-type: fs
          format: sarif
          output: trivy.sarif

      - name: Trust PYXA's certificate
        if: env.PYXA_CA_CERT_PEM != ''
        run: |
          printf '%s\n' "$PYXA_CA_CERT_PEM" > pyxa.crt
          echo "PYXA_CA_CERT=$PWD/pyxa.crt" >> "$GITHUB_ENV"

      - name: Send to PYXA
        run: |
          curl -fsS ${PYXA_CA_CERT:+--cacert "$PYXA_CA_CERT"} "$PYXA_URL/downloads/pyxa.py" -o pyxa.py
          python3 pyxa.py push trivy.sarif --wait

A step's if cannot read secrets directly, which is why the certificate passes through the job's environment.

The job fails when the import fails (exit 1) or the upload is refused (exit 2), so a broken feed shows up as a red run rather than as findings that silently stop arriving.

GitLab CI

Under Settings, CI/CD, Variables: PYXA_UPLOAD_KEY (masked, protected), PYXA_URL, PYXA_CONNECTOR, and, if PYXA signs its own certificate, PYXA_CA_CERT as a File variable holding the certificate. GitLab then sets PYXA_CA_CERT to the file's path, which is what pyxa expects.

# .gitlab-ci.yml
stages: [scan, report]

semgrep:
  stage: scan
  image: semgrep/semgrep
  script:
    - semgrep scan --config auto --sarif --output semgrep.sarif || true
  artifacts:
    paths: [semgrep.sarif]

send-to-pyxa:
  stage: report
  image: python:3.12-slim
  needs: [semgrep]
  script:
    - python3 -c "import os,ssl,urllib.request as u; c=ssl.create_default_context(cafile=os.environ.get('PYXA_CA_CERT') or None); open('pyxa.py','wb').write(u.urlopen(os.environ['PYXA_URL']+'/downloads/pyxa.py', context=c).read())"
    - python3 pyxa.py push semgrep.sarif --wait
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

python:3.12-slim has no curl, so the first line fetches the client with Python itself. On an image with curl, use the GitHub Actions line.

|| true after Semgrep keeps the job going when it finds something, which is when the file matters most.

A scheduled job

For an export a tool writes on a schedule, such as an EASM export or a SharpHound collection, a cron entry on a machine that holds the file is enough:

# /etc/cron.d/pyxa-easm: send the night's export at 06:10
10 6 * * *  pyxa  . /etc/pyxa/upload.env && /usr/bin/python3 /opt/pyxa/pyxa.py push /srv/exports/easm.json --wait >> /var/log/pyxa-push.log 2>&1

/etc/pyxa/upload.env holds the export PYXA_...= lines. Make it readable only by the account that runs the job.

A SharpHound collection is sent as its .zip. Each upload is kept in its own folder, so an older collection is never read together with a newer one.

What PYXA refuses, and why

Nothing is stored when a request is refused.

HTTPpyxa exitReason
4012no key, a key that does not exist or was revoked, a key whose maker was deactivated, or an API or MCP token instead of an upload key
4032an upload key made for another connector; the message names both
4002a file type the connector does not read (the message lists the ones it does), an empty file, no file field, or a CSV kind other than vulns, assets or kb
4092the connector is disabled, or is importing already. Send the file again once the import has finished
4132the file is over the limit: PYXA_UPLOAD_MAX_MB, 200 MB by default

The upload key opens nothing else. The public API (/api/v1/) and the MCP server refuse it, and the upload API refuses their tokens.

PYXA keeps the newest ten files per connector on disk and deletes older ones; every receipt stays, with its file's name, size, SHA-256, the address it came from, and what the import made of it.

Looking after the server: pyxa on the PYXA host

The same pyxa is on the PATH inside the django container, where it also manages the installation. It runs as the application's user, so the files it writes stay writable by PYXA.

docker compose exec django pyxa doctor
CommandWhat it does
pyxa connectors listevery connector with its id, type, state and last sync, and the error of any that is failing
pyxa connectors test <id>test one connection; for a file connector, says whether the path is missing, outside the import folder, or empty
pyxa connectors sync <id>run one connector's import now and print its summary
pyxa keys make <id>make the connector's upload key and print it once; --as-user <name> records who it belongs to, otherwise the first superuser
pyxa keys revoke <id>revoke it
pyxa uploads <id>the connector's last ten uploads, with each one's outcome
pyxa folderswhether the import folder exists and is writable, its free space, and whether every file connector's path is readable
pyxa doctorthe start-up checks (manage.py preflight), the folders, and every failing connector
pyxa merge [--dry-run]merge assets that several scanners report as one (manage.py merge_duplicate_assets)
pyxa rescorerecompute every open finding's risk score after a scoring change (manage.py rescore_findings)

Every command exits 1 when something it checked is wrong, so pyxa doctor in a monitoring job alerts on a full disk, a missing volume or a connector that has stopped importing.

Run outside PYXA, a server command says where it belongs instead of failing obscurely:

pyxa: 'doctor' runs on the PYXA server, where it can reach the database: docker compose exec django pyxa doctor

Generated from docs/file-push.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.