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
- 1. Make the connector's upload key
- 2. Send a file with pyxa
- Send a file with curl or PowerShell
- PYXA with its own certificate
- GitHub Actions
- GitLab CI
- A scheduled job
- What PYXA refuses, and why
- Looking after the server: pyxa on the PYXA host
Which way to send files
| Push over HTTPS (this page) | Shared folder | |
|---|---|---|
| What the sender needs | the PYXA address and one upload key | write access to a share mounted at the import folder |
| What it can reach | one connector's upload, nothing else | every file in the import folder |
| When the import runs | as soon as the file arrives | at the connector's next schedule |
| What the sender learns | the import's own summary, and an exit code | nothing; 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.pySettings come from the environment, or from options with the same names:
| Variable | Option | What it is |
|---|---|---|
PYXA_URL | --url | PYXA's address, e.g. https://pyxa.example.com |
PYXA_CONNECTOR | --connector | the connector's id, shown on its page |
PYXA_UPLOAD_KEY | --key | the connector's upload key |
PYXA_CA_CERT | --ca-cert | PYXA'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 --waittrivy.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 --waitExit codes, for a pipeline to act on:
| Code | Meaning |
|---|---|
| 0 | imported (or, without --wait, received) |
| 1 | the import ran and failed; the summary says why |
| 2 | refused before anything was stored, or a setting is missing; the message says which |
| 3 | still 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.messageWindows 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_URLandPYXA_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 --waitA 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_BRANCHpython: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.
| HTTP | pyxa exit | Reason |
|---|---|---|
| 401 | 2 | no 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 |
| 403 | 2 | an upload key made for another connector; the message names both |
| 400 | 2 | a 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 |
| 409 | 2 | the connector is disabled, or is importing already. Send the file again once the import has finished |
| 413 | 2 | the 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| Command | What it does |
|---|---|
pyxa connectors list | every 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 folders | whether the import folder exists and is writable, its free space, and whether every file connector's path is readable |
pyxa doctor | the 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 rescore | recompute 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 doctorGenerated from docs/file-push.md in the PYXA repository.