Skip to content

Network Testing

Ratomir Jovanovic edited this page Oct 29, 2026 · 7 revisions

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.

1. Write the scope manifest

pandaward scope init

Edit .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 job

Validation 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.

2. Approve it yourself

pandaward scope validate
pandaward scope approve --by "Jane Doe" --hours 8

Approval 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.

3. Run

pandaward test --profile production-observe
  • At most max_requests_total GET requests, at requests_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.

4. ZAP baseline (staging-active)

pandaward doctor                         # shows whether ZAP was found
pandaward test --profile staging-active

Install 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_paths as exclude paths. The jobs are the spider, the passive scan, the active scan if zap.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.1 with a random port. The proxy terminates TLS with a throwaway certificate and sends every request through the same scope guard as production-observe: origin, excluded paths, allowed_methods, max_requests_total, requests_per_second and 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.

5. Authorization checks (staging-active)

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-active

The 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:

  1. The request is sent as each allowed identity. All of them must get a 2xx answer; otherwise the check is INCONCLUSIVE with a note, because a denial for everyone else proves nothing.
  2. It is then sent as every other identity and without credentials (anonymous, which you can also list in allowed).
  3. 401, 403, 404, other 4xx and redirects (never followed) count as denied. A 2xx is a finding: CONFIRMED when the body is identical to what an allowed identity got, STRONGLY_SUPPORTED when it differs. 429, 5xx and errors make the check INCONCLUSIVE.
  4. Answers for identities marked revoked decide 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.

Clone this wiki locally