Recording real traffic¶
apiverity capture --target https://staging.example.com --out corpus.har
# point a client at the address it prints; ctrl-c to stop
apiverity drift openapi.yaml --corpus corpus.har
drift --corpus and infer both want real traffic, and
until now the only way to get some was to already have a HAR from somewhere
else. This records one.
Redaction happens before the write¶
A recorder that wrote the HAR and then sanitized it would have already put an
Authorization header on disk — and on a crash between the two, left it there.
So headers, query strings, request URLs and bodies are redacted in memory, and the entry appended to the log is the redacted one. The unredacted response still goes back to the client, because the client asked for it and this is a proxy.
The URL is rebuilt from the redacted parameters rather than carried through.
Redacting queryString and leaving request.url alone writes the credential
into the file anyway, one field over — which is exactly what the first version
of this did, and what the test that reads the file back caught.
A JSON body gets two passes: the field-name rules, then the patterns over each
string value. The second catches a credential sitting inside an opaque
string — a request body echoed back, a log line carried in a field — which
nothing keyed on field names can see into. Over the values rather than the
serialized document, because in serialized form {"password": "x"} nested in a
string reads as password\": \"x, and the escaping is what hides it.
What redaction cannot catch¶
An unlabelled secret in free text: a bare token with nothing beside it saying what it is.
Redaction here is rule-based, not clairvoyant. A recorder claiming otherwise is the claim that gets a credential committed. Read what you captured before you commit it.
What it refuses to be¶
An open relay. Every request goes to the one --target the run named. A
client configured to use a proxy writes the whole URL on the request line --
GET http://somewhere-else/ HTTP/1.1 -- so that is where an open relay comes
from, and it is checked rather than assumed: a request naming any other scheme
or host gets a 403 and is counted as off_target. The absolute form for the
target itself still works, because that is what a browser sends.
Reachable from the network. It binds 127.0.0.1 and refuses any other
address without --i-know-this-is-exposed. An unauthenticated recording proxy
on a routable address is a credential collector somebody else can point
wherever they like — and this one writes what it sees to a file.
A TLS interceptor. There is no CONNECT. Tunnelling would either pass TLS
through opaquely, recording nothing, or require issuing certificates for hosts
this process does not own. It answers 405 and says which.
What it admits it did not record¶
A corpus that silently drops what it could not handle has gaps that read as
facts about the service. Every skip is counted, named in the JSON output, and
written into the HAR's own log.comment so it survives the file:
| Skip | Why |
|---|---|
oversized_request / oversized_response |
over --max-body-bytes (default 256 kB) |
binary_request / binary_response |
a content type with no useful text in a HAR |
upstream_failed |
the target never answered; the client got a 502 |
after_limit |
--max-entries was reached and traffic kept arriving |
off_target |
a request named a host that is not --target; the client got a 403 |
--max-entries is a hard cap, not a threshold: asking for two gives two. The
poll loop notices a limit a tenth of a second later, by which time more requests
have arrived, so the cap is enforced where the entry is appended — and what
arrived after it is counted rather than silently absent.
An empty corpus is still written¶
Nobody sent anything through: that is a fact about the run. A missing file reads as a crash.
Options¶
--target URL |
the one upstream to forward to (required) |
--out FILE |
where to write the HAR (required) |
--host / --port |
where to listen (default 127.0.0.1, any free port) |
--duration SECONDS |
stop after this long |
--max-entries N |
record at most this many, then stop |
--max-body-bytes N |
bodies above this are noted as absent, with the reason |
--no-response-bodies |
request bodies only — narrows what the corpus is usable for, since drift --corpus compares responses |