Repository navigation
Network Testing
Two profiles send traffic, from v0.1.5:
| Profile | What it does | Decides | Targets |
|---|---|---|---|
production-observe |
one GET per origin, review of response headers and cookie flags | H-003 | any environment |
staging-active |
authorization checks with test identities, then a ZAP spider and passive scan; the ZAP active scan only when the manifest allows it | C-001, C-002, B-004, M-001, D-005 |
staging or local only |
local-runtime checks app storage on an emulator or simulator and sends no traffic; see Mobile Runtime.
pandaward scope initEdit .pandaward/scope.yaml yourself:
schema_version: 1
authorization:
owner: "Example Ltd"
reference: "SEC-123 written approval from the system owner"
purpose: "Header review before release"
contact: "security@example.com"
acknowledged: true
environment: staging
window:
start: "2026-11-01T08:00:00Z"
end: "2026-11-01T18:00:00Z"
targets:
- url: https://staging.example.com
excluded_paths: []
allowed_methods: [GET, HEAD]
allow_private_addresses: false
limits:
requests_per_second: 1
max_requests_total: 20
timeout_seconds: 10
zap:
active_scan: false # true sends ZAP attack payloads (staging-active only)
max_duration_minutes: 10 # limit per ZAP jobValidation rejects wildcards, credentials in URLs, query strings, trailing-dot hosts, unknown fields, unsupported methods and out-of-range limits. The shipped template is deliberately invalid.
pandaward scope validate
pandaward scope approve --by "Jane Doe" --hours 8Approval works only from an interactive terminal and asks you to type a code derived from the manifest's SHA-256. Any edit to the manifest invalidates it. pandaward scope revoke cancels approvals.
Approvals are also bound to a per-user key outside the project (pandaward/approval.key in your user configuration directory). A scope file or ledger that arrives with a repository, a bundle or a copied .pandaward/ directory therefore never counts as approved; approve again on the machine that runs the test.
pandaward test --profile production-observe- At most
max_requests_totalGET requests, atrequests_per_second. - Redirects are not followed; a redirect outside scope is reported.
- Every connection resolves and checks the address first. Loopback, private, link-local and CGNAT ranges are refused unless
allow_private_addresses: true; cloud metadata addresses are always refused. - No proxy from the environment is used.
- Cookie values are never recorded, only names and attributes.
Results update control H-003 and its findings in the latest assessment.
pandaward doctor # shows whether ZAP was found
pandaward test --profile staging-activeInstall ZAP yourself; it needs Java 17 or later. PandaWard looks for zap.sh, zap.bat or zaproxy on PATH, then in the default install location, and runtime.zap_path in .pandaward/config.yaml overrides both (see Configuration). PandaWard never installs or downloads ZAP.
What happens:
- PandaWard writes a ZAP automation plan with one context: the manifest targets as include paths and
excluded_pathsas exclude paths. The jobs are the spider, the passive scan, the active scan ifzap.active_scan: true, and a JSON report. - ZAP runs headless with
-silent(no update checks or other unsolicited requests) and a disposable home directory under.pandaward/private/runs/. - All ZAP connections are chained to a PandaWard proxy on
127.0.0.1with a random port. The proxy terminates TLS with a throwaway certificate and sends every request through the same scope guard asproduction-observe: origin, excluded paths,allowed_methods,max_requests_total,requests_per_secondand the address checks. PandaWard checks the real target's certificate. - Links to other sites are refused by the guard and counted, never sent. They are not findings.
- The guard stops after three consecutive 429 or 5xx answers.
Results:
| Outcome | Status |
|---|---|
| ZAP not installed | BLOCKED |
| ZAP failed, timed out or wrote no report | ERROR |
| No request reached the target | INCONCLUSIVE |
| Budget exhausted or guard stopped, no confirmed alert | INCONCLUSIVE |
| Alerts with high or confirmed confidence | FAIL |
| Only medium or low confidence alerts | SUSPECTED |
| Complete run without alerts |
PASS for this check only |
Each ZAP alert type becomes one finding per origin (zap.<plugin id>), with up to 20 redacted instances in its evidence. With the active scan, injection classes (SQL, command, code and template injection, XSS, path traversal, XXE and similar) go to D-005 and everything else to M-001. A complete run that no longer raises an alert marks its finding REMEDIATED.
Allowed methods stay GET, HEAD and OPTIONS, so the active scan only tests what those methods reach.
staging-active runs the manifest's authorization checks before ZAP, through the same guard and budget. You need test accounts on the staging system and objects or routes that exist for the test.
identities:
- name: alice
tenant: acme
header: Authorization
value_env: PANDAWARD_ID_ALICE
- name: bob
tenant: beta
header: Authorization
value_env: PANDAWARD_ID_BOB
- name: carol # an admin of acme
tenant: acme
header: Authorization
value_env: PANDAWARD_ID_CAROL
- name: alice-logged-out # a token taken before logout
tenant: acme
header: Authorization
value_env: PANDAWARD_ID_OLD
revoked: true
authorization_checks:
- name: acme invoice
kind: object # decides C-001
method: GET
path: /api/invoices/inv_acme_1
allowed: [alice, carol]
- name: user admin
kind: function # decides C-002
method: GET
path: /admin/users
allowed: [carol]Set the credentials in your own terminal, then run the test:
export PANDAWARD_ID_ALICE="Bearer eyJ..." # PowerShell: $env:PANDAWARD_ID_ALICE = "Bearer eyJ..."
export PANDAWARD_ID_BOB="Bearer eyJ..."
export PANDAWARD_ID_CAROL="Bearer eyJ..."
export PANDAWARD_ID_OLD="Bearer eyJ..."
pandaward test --profile staging-activeThe variable holds the full header value, for example Bearer <token> for Authorization or session=<id> for Cookie. It is read when the test starts and never written anywhere. If any variable is empty, the run stops before the first request.
How each check is judged:
- The request is sent as each allowed identity. All of them must get a 2xx answer; otherwise the check is
INCONCLUSIVEwith a note, because a denial for everyone else proves nothing. - It is then sent as every other identity and without credentials (
anonymous, which you can also list inallowed). - 401, 403, 404, other 4xx and redirects (never followed) count as denied. A 2xx is a finding:
CONFIRMEDwhen the body is identical to what an allowed identity got,STRONGLY_SUPPORTEDwhen it differs. 429, 5xx and errors make the checkINCONCLUSIVE. - Answers for identities marked
revokeddecide B-004 instead.
| Control | Decided by |
|---|---|
| C-001 object-level authorization (BOLA) |
kind: object checks |
| C-002 function-level authorization |
kind: function checks |
| B-004 session revocation | identities with revoked: true, on every check |
Checks only use GET or HEAD, so they change no data and need no cleanup. Each check costs one request per identity plus one anonymous request; validation rejects a manifest whose checks need more than max_requests_total. Validation also rejects unknown or revoked identities in allowed, paths with .., targets outside the manifest and reserved headers such as Host.
Evidence stores, per identity, the HTTP status, body size, content type and a body hash. Request headers, response bodies and credentials are never recorded. A complete re-run in which access is denied marks the finding REMEDIATED.
B-003 (login, reset and MFA flows) and G-002 (API contract against runtime) are not covered by these checks and stay UNSUPPORTED. staging-active refuses a manifest with environment: production, and validation rejects zap.active_scan: true there.
PandaWard is AGPL-3.0-only. Not a certification or signed penetration test. Report a vulnerability.
Getting started
Using
- CLI Reference
- Claude Code Plugin
- Findings Lifecycle
- Retest and Delta
- Coverage and Statuses
- Reports
- Evidence Bundles
- Network Testing
- Mobile Runtime
- Configuration
- CI Integration
Reference