Repository navigation
test(network): cover outbound proxy/TLS config, resolver and replay routing - #153
Open
lorenzozanee wants to merge 1449 commits into
Open
lorenzozanee wants to merge 1449 commits into
lorenzozanee wants to merge 1449 commits into
Conversation
- 001: SSH target enumeration - 002: SSH tunnel establishment - 003: SSH command execution/pivot
- AutoLogon: full password kept in output (Category C, agent may use it), masked in findings details - VNC: raw registry output replaced with length indicator
network.ts keeps 001 (single target scan), lateral.ts gets 002 (subnet discovery).
…breakdown cloud-security.txt AWS section expanded from 7 bullets to full 7-category listing matching azurehook detail level.
Add 10 missing programs to kill chain phases and reference table: multi_region_scan, kms_enum, opensearch_enum, efs_enum, elb_enum, cloudtrail_selector_tamper, backup_vault_enum, cloudwatch_logs_dump, sns_sqs_siphon, kinesis_tap.
…nstead of silent exit When a provider's safety classifier blocks a response, the session loop exited silently with no error shown — appearing as if CyberStrike hung. Now sets an error on the assistant message so TUI/web renders a visible notice, and logs at WARN level instead of INFO for diagnosability. Closes CyberStrikeus#62
…Strikeus#58) Guard clause in provider.ts now checks for providerConfig — when a user explicitly configures amazon-bedrock in their config, the provider loads even without explicit env var credentials, allowing fromInstanceMetadata() to resolve IAM role credentials. Fixes CyberStrikeus#58
…S fallback fromNodeProviderChain covers all credential sources: env vars, SSO profiles, ~/.aws/credentials, credential_process, web identity, and IMDS (EC2 instance roles). Falls back to fromInstanceMetadata if the chain import fails under Bun.
Passive questions like "what is the tech stack?" were triggering ensure_tools to install 22 attack tools on smaller local models (e.g. Ollama qwen2.5-coder:14b). The methodology prompt gave an unconditional "Use ensure_tools before testing" instruction that small models followed blindly for any prompt. Closes CyberStrikeus#43
Defense-in-depth: tool description now tells the model to NEVER call ensure_tools for passive/informational requests — reinforces the methodology prompt change for models that weight tool descriptions more than system prompt instructions.
OrcaRouter is already functional via the models.dev catalog (openai-compatible SDK, 81 models). Adding it to the provider table for discoverability. Closes CyberStrikeus#64
OrcaRouter is one of 160+ providers in the auto-generated models.dev snapshot — no dedicated code, no tests, no active support. The README table lists core integrations we actively maintain, not every catalog entry. Reopens CyberStrikeus#64
Commands that produce binary output (e.g. raw exe content via WSL, cat on binary files) were dumping non-printable bytes directly into the TUI, corrupting the terminal display with garbled ANSI sequences. Adds a per-chunk binary detection heuristic (>30% non-printable bytes) that strips binary data and replaces it with a placeholder message. Normal text output including ANSI colors and UTF-8 passes through. Fixes CyberStrikeus#38
`{env:VAR}` keeps a proxy password out of the config file, but the loader
expands it into the in-memory config — which GET /config returned verbatim and
`debug config` printed. The field description promises the file, and a reader
can easily hear that as a promise about the process.
Both display paths now go through Config.redactSecrets: the proxy password and
client-certificate passphrases come out as "[redacted]" while the field stays
visible, so it is still obvious that one is configured. Everything non-secret
(username, url, certificate paths) is untouched.
The redactor copies rather than mutates — get() hands back the live cached
config, so masking in place would blank the credential the running process
still needs. Verified explicitly.
Scope: only the secrets this feature introduced. Provider API keys ride the
same channel and are equally exposed, but that predates this work and the TUI
consumes that response — a behaviour change there belongs in its own issue.
Verified through the real route (Request in, Response out): the secret is live
in the process, absent from the response, and still live afterwards.
Refs CyberStrikeus#102
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…red proxy These scripts send real traffic at the target and were the last senders ignoring the proxy entirely — which mattered more than it looks: the bash denies on curl/wget/python-requests only just became effective, making attack_script the path of least resistance for unproxied HTTP. Analysis of what actually had to be covered, per script: jwt_tamper no network nothing to do ssrf_listener inbound only nothing to do race_tester aiohttp IGNORES the environment file_upload_tester requests honours it subdomain_takeover requests honours it cloud_storage_enum requests honours it wayback_endpoints requests honours it github_dorker gh CLI honours it So environment variables alone would have covered six of seven and missed race_tester — the most target-facing one of the set. It needed trust_env=True. It was also the only script still verifying TLS while its four siblings all pass verify=False, so behind an intercepting proxy it would have been the one script that broke the moment a proxy was configured; it is now aligned with them and with http_replay's own default. Environment variables are the right mechanism HERE and the module says why: inside our own process an ambient proxy variable would silently capture traffic we never routed, so callers pass options explicitly — but a third-party script has no such seam, the variables ARE the interface. Both cases of each name are set, and NO_PROXY carries the bypass list in the same exact+suffix pairs used for the browser. Verified by running the real tool (real python3, real proxy), covering both client families: aiohttp and requests each reach an otherwise-unresolvable host through the proxy, neither touches it when no proxy is configured, and the bypass list still excludes a host. Refs CyberStrikeus#102 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nreadably Handed a socks:// URL in HTTP_PROXY, the scripts' clients speak HTTP at a SOCKS server and report "400, message=Expected HTTP/" — an error that names neither the proxy nor the setting that caused it, sending the reader to look at the target. The tool now stops before spawning and says which setting is the problem and what to do about it. Measured first, because the answer changed what to build: the traffic does NOT leak past the proxy in that state — with a resolvable non-loopback target and a SOCKS proxy configured, the scripts reach it zero times versus 2 and 27 with no proxy at all. So this is a legibility fix, not a containment one. (Two earlier attempts at that measurement were wrong: an unresolvable host cannot tell "refused" from "went direct and failed DNS", and a loopback target is bypassed by design.) childEnv throws here where forHost only warns, and the asymmetry is deliberate: a replayed request fails closed on its own, so returning the proxy keeps the safe behaviour, while a child process handed the same URL produces garbage. Refusing to start is the only outcome that is both safe and explicable. Verified across every proxy configuration, both client families (aiohttp and requests), real python3 and real proxies: open, authenticated, wrong credentials, intercepting (TLS decrypted end to end), SOCKS, disabled, bypassed and dead — 16/16. Refs CyberStrikeus#102 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Corporate proxies commonly demand NTLM or Negotiate, which nothing here can satisfy — only Basic is sent. Measured against a Digest-challenging proxy: one attempt, Basic offered, 407 returned, no hang and no retry loop. That part is fine; the problem was the report. A bare 407 in the tool output reads as an authentication finding about the ENDPOINT, and a tester agent would record it as one. The result now carries a note naming the proxy authority (never the credentials) and the scheme the proxy actually asked for, so an unsatisfiable enterprise proxy is legible instead of looking like a target behaviour. Carried on the result rather than added at each call site, so every output path picks it up. Also measured while covering enterprise-shaped setups, all as expected: - https:// proxy (TLS to the proxy itself) works — the scheme the validator accepts is not a dead option - NODE_EXTRA_CA_CERTS is honoured, so a corporate CA installed system-wide reaches the replay backends without being repeated in caPath. This matters: the fetch stack does NOT read the OS keychain, so without it caPath would have been the only route - an IPv6 proxy address works Refs CyberStrikeus#102 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Measured on the pinned Playwright: setting clientCertificates makes EVERY page load in that context hang until timeout. Reproduced against bare chromium.launch() with none of our launch args, with a server certificate signed by a trusted CA, and with a server that never requests a client certificate — so it is neither a trust problem nor a mutual-TLS problem. Playwright inserts a local interceptor whenever the option is present and that interceptor never completes. One entry re-routes the whole context, so a plausible config would have silently killed a crawl on hosts the certificate had nothing to do with. forBrowser() now warns instead of emitting the option. Replayed requests still honour client certificates (verified on the wire), so the capability is not lost — only the crawler cannot reach a mutual-TLS host, and it now fails fast with ERR_BAD_SSL_CLIENT_AUTH_CERT rather than hanging. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tial-crawl diagnosis (CyberStrikeus#105) The worker already listened for browser.disconnected / page.close / page.crash, but logged a terse one-line message with no context, and never logged a popup/new tab opening — so a crawl that stopped early left little to diagnose from. Add a guarded lifecycleContext(browser, page?) helper (url, open-page count, isConnected — every read wrapped so a crash/disconnect handler can never throw while gathering diagnostics) and attach its output to the existing terminal logs on BOTH the single- and multi-credential paths. Also add a context "page" listener that logs (debug) when a popup/tab opens, with the same context — no such listener existed. Scope kept proportionate: no dedicated per-session log file (worker logs are already persisted via the parent's log pipeline, and CyberStrikeus#104 removed the main mid-crawl crash, so the crash-window loss the file would guard is now rare), no verbosity flag, no crawl-progress plumbing. Verified: a real crawl that opens a popup logs "new page/popup opened" with {url, openPages, connected} and the terminal logs now carry the same context; the guarded reads don't throw even on a not-yet-committed popup URL or a disconnected browser; 99/99 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t_probe http_replay resolved the proxy/TLS transport, applied governance and framed proxy errors inside its own sendGoverned; inject_probe (after CyberStrikeus#106) composed the same Send.governed(() => BackendFetch.send(...)) inline — but without the Network.forUrl step, so it sent DIRECTLY, bypassing a configured proxy. Two send paths that must agree on every outbound attack request had already drifted. Extract the one thing every target-facing tool must do identically into `src/tool/web-send.ts` — `WebSend.send(msg, origin, opts)`: - resolve Network.forUrl (proxy, extra CA, client cert, TLS strictness) ONCE, - the rejectUnauthorized precedence (per-call insecureTls -> config -> accept-bad-certs), - Send.governed with the caller's governor(s), - the pure BackendFetch transport (primitives only — the backend stays config-free), - the proxy-aware error annotation and the 407 proxyNote. Both tools now route through it: http_replay.sendGoverned is a thin adapter that only supplies its per-call governor + parameter names; inject_probe passes its per-battery governor. inject_probe therefore honours the configured proxy for the first time, and — the point — any future change to the send (a TLS rule, an auth step, an observability hook) reaches both tools at once instead of one silently drifting. Per-tool concerns (credential, observation, mutations/battery) stay in the tools; only putting a request on the wire is shared. DAST intent: every attack request goes through one core request service. Backend purity is preserved (WebSend passes primitive fields to BackendFetch, which still needs no config/Instance context); the hackbrowser worker's own transport is untouched (separate process). Verified against live Juice Shop + a byte-level echo capture: inject_probe still finds the SQLi (50 governed sends) and injection payloads (quotes, OR/||) arrive intact through the new path; http_replay's request_id and target paths still return 200. Typecheck clean in both packages. Refs CyberStrikeus#102, CyberStrikeus#106
Found by an end-to-end proxy test campaign (78 scenarios, real local proxies) against the WebSend refactor. The core inject_probe-through-proxy path was green (payloads byte-intact, all classes transit the proxy); these are the three bugs: 1. http_replay_raw ignored config's tls.rejectUnauthorized (HIGH, dead-config TLS downgrade). The raw path forwarded net.ca/clientCertificate but hard-coded `rejectUnauthorized: insecure_tls === false`, dropping the resolved strictness, so a global strict-TLS setting was silently ignored for every byte-exact send. Fix: a single source of truth — Network.tlsRejectUnauthorized(insecureTls, out), per-call -> config -> default — now used by BOTH WebSend and the raw path, so the security precedence cannot drift between them. 2. A malformed/unsupported proxy URL threw out of execute() instead of returning a clean error (MED). WebSend now catches the Network.forUrl throw and fails CLOSED with a structured error — never a direct send that leaks past the proxy — fixing http_replay and inject_probe at once. 3. redactSecrets did not mask credentials embedded in proxy.url (MED, secret disclosure through GET /config). It masked proxy.auth.password only; now it also blanks a password inside proxy.url, on the copy (never mutating cached config). Deferred (reported, not fixed here): cert file-cache staleness on rotation (low, acknowledged by-design) and http_replay sweep/compare using a per-send governor (tied to CyberStrikeus#91, not a proxy bug). Refs CyberStrikeus#102
…srf through WebSend
Follow-up review of the WebSend refactor found three related gaps — the first a
serious false-negative on the DAST backbone, opened BY the refactor (inject_probe
is now proxied):
1. inject_probe read a proxy 407 as a target response (HIGH). looksBlocked covered
403/406/429/503 but not 407, and proxyNote was never read — so against an
auth-refusing proxy every payload got a 407, marker "not reflected", and the
battery closed "nothing found", indistinguishable from a genuinely safe endpoint.
WebSend already produced the explanation (proxyNote); inject_probe dropped it.
Fix: surface result.proxyNote as a stop signal in send(), and add 407 to
looksBlocked for a bare 407 that reaches the observation.
2. csrf_extract was the last target-facing tool outside WebSend — its own
BackendFetch.send + Network.forUrl composition, so the fail-closed fix for a bad
proxy.url did not reach it (raw throw out of execute()), it had no governance, and
its rejectUnauthorized defaulted the opposite way to the others (verify-ON vs the
shared helper's accept-bad-certs — csrf failed on a self-signed target the other
tools accept). Route it through WebSend: proxy/TLS/fail-closed/governance/precedence
now identical, and WebSend is the single core sender for all three attack tools.
3. proxyNote was an untyped side-channel (`as typeof result` on the write, an ad-hoc
`as { proxyNote?: string }` on the read) — which is exactly why (1) was easy to
miss. Add `proxyNote?: string` to Send.Result; the writer needs no cast and the
reader references the real type, so a future rename/typo is compiler-visible.
Typecheck clean. Detailed re-test (incl. 407-on-inject_probe and csrf-through-proxy,
the coverage the first campaign missed) to follow.
Refs CyberStrikeus#102
…ks (CyberStrikeus#87 root cause 2) collectDOMLinks only queried a[href], so on SPAs where navigation is a button with an onclick handler (or a data-href element) it found 0 links and BFS discovery depended entirely on the planner happening to click each button — which it does not do exhaustively, so pages behind unclicked buttons were never visited. Widen the query to a[href], [data-href], [onclick] and additionally extract the target from the unambiguous navigation forms: data-href, and onclick handlers that assign location / call location.assign / window.open. Deliberately NOT navigate()/push()/go() — those collide with analytics.push and Array.push and would enqueue phantom pages. Extracted targets flow through the existing enqueue path (normalizeUrl + visitedPages dedup + per-path cap), so no new control flow. The anchor branch is unchanged, so this is purely additive to discovery. Verified in practice: a pure button-nav SPA (no a[href], 10 views) went from 5/10 views reached to 10/10; a normal a[href] multi-page site is unaffected (anchor path untouched); 99/99 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…directs (CyberStrikeus#87 root cause 3) The multi-credential access-denied filter used /\/(login|…|auth)/i to decide whether a redirect landed on a login/denied gate and should be skipped. The bare `auth` alternative matched legitimate resources — /auth/profile, /auth/settings, even /authors/123 — so those pages were wrongly skipped (coverage loss) and, in the per-credential access-control diff, mislabeled. Replace bare `auth` with `authenticate`. Real /auth/login and /auth/signin gates are still caught via the existing login/signin terms; only bare /auth (rare, and low-cost to explore) is no longer force-skipped. This biases toward fewer false positives, which is the right trade — a wrongly skipped page loses coverage, while a wrongly explored login page costs one page. Verified against 11 URL cases: old regex produced 3 false positives (/auth/profile, /auth/settings, /authors/123); new regex has 0 false positives and 0 false negatives (all login/signin/auth-login/unauthorized/access-denied/authenticate gates still match). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…etch — fixes Copilot crawls The Copilot request path in the hackbrowser worker subprocess set its User-Agent with a bare `CYBERSTRIKE_VERSION` — a build-time define that the standalone worker bundle build never injects. In the subprocess that identifier is undefined, so the header line threw `ReferenceError: CYBERSTRIKE_VERSION is not defined` on every planner LLM call, before the fetch even ran; planPage then returned an empty plan for every page and the crawl "completed" immediately with no exploration. Only surfaces when a GitHub Copilot model is used — other providers never enter this branch. Guard it exactly like Installation.VERSION already does (installation/index.ts): `typeof CYBERSTRIKE_VERSION === "string" ? CYBERSTRIKE_VERSION : "local"`, so it falls back to "local" instead of throwing. The main-process chat path uses this same guarded accessor, so the worker now matches it. No token-exchange/header changes are needed: chat and worker both authenticate with the raw token and equivalent headers, and chat works — aligning the User-Agent is sufficient. Verified: the bare reference throws ReferenceError; the guarded form returns "cyberstrike/local" with no throw; cyberstrike typecheck clean. Related: CyberStrikeus#107. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…the crawler worker The worker built every Copilot model with sdk.languageModel(), which the Copilot SDK maps to the Chat Completions endpoint. GPT-5+ Copilot models are only served on the Responses API — the main process already routes them there via shouldUseCopilotResponsesApi — so GPT-5 planner calls hit the wrong endpoint and every plan failed. Mirror the main-process selection: GPT-5+ -> sdk.responses, others -> sdk.chat (identical to the old languageModel for non-GPT-5, so those are unchanged). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The worker runs as a separate Node subprocess and makes its own TLS connection to the LLM API. Node trusts only its bundled CA list, so behind a TLS-intercepting proxy / VPN / antivirus (a root CA the OS trusts but Node's bundle does not) the worker's LLM call failed with UNABLE_TO_GET_ISSUER_CERT_LOCALLY -- "planPage failed: cannot connect to API: unable to get local issuer certificate" -- while the main-process chat with the same token worked. Merge the OS trust store into the default CA set at worker startup (what `node --use-system-ca` does), in-process. It only adds trust, so it cannot break a connection that already verifies; on runtimes without the APIs (bun, Node < 22.15) it is a graceful no-op. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rawler worker createModelFromDescriptor now emits an info log naming the endpoint (responses / chat / languageModel) each Copilot model routes to, so an operator can confirm from the crawl log that GPT-5+ models use the Responses API and spot an endpoint mismatch after the fact. Routing is unchanged; only the log line is added. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Only model calls went through the proxy. The OAuth refreshes that keep those calls alive did not, nor did the device flows, nor llmhook — each is a plain fetch of its own. Behind a proxy-only egress that meant the chat worked and the hourly token refresh failed, reported as "oauth token refresh failed" with nothing pointing at the proxy. The obvious fix — a network-aware helper swapped in at each of the 12 known call sites — was rejected. It makes coverage a matter of discipline: the next provider plugin writes a plain fetch and silently escapes, and third-party plugin code we do not control never had the helper to begin with. That exact failure already happened here once: inject_probe moved onto a new send path and went direct while the operator believed everything was proxied. So route the process's own fetch once, from the CLI entry point: - Network.installGlobalTransport() wraps globalThis.fetch. Every present and future call site is covered, including code we do not own. Nothing to remember. - An init that already carries proxy/tls is passed through untouched — the replay backends route themselves and must not have policy applied twice. - Reading the config can itself fetch (a remote well-known config), which would re-enter the hook and await the load it sits inside. Guarded, and direct is the only possible answer there: a config-derived proxy cannot route the request that loads that config. - No Instance context or an unreadable config falls through to a plain fetch, so an early CLI path behaves exactly as if the hook were absent. - Installed from index.ts, not on import: a global side effect at import time would reach unit tests and anything embedding this package. provider.ts's own copy of the transport resolution is deleted — it is now the same rule as everywhere else rather than a duplicate that could drift. Renamed includeProviders to includeInternal: the setting no longer describes providers alone but all of CyberStrike's own outbound traffic. Unreleased. Verified on the wire (9 cases): off stays direct; on proxies a plain fetch; an explicit per-call proxy wins with no double-apply; a tls-only init is untouched; the bypass list and loopback still apply; no Instance context falls through; three concurrent fetches during a first config load all complete, no deadlock. Provider path 3/3, worker subprocess 4/4, resolver 15/15, main matrix 23/23, suite 1663 pass (the one failure predates this branch). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… chain The guard was a module-level boolean held across the config read. That looks equivalent to a reentrancy check and is not: while one request resolves the config, every other request that arrives sees the flag set and goes DIRECT. Measured, 12 parallel fetches with the proxy configured and includeInternal on: 1 proxied, 11 silently direct — cold config and warm alike. That is exactly the failure this whole module exists to prevent, and my own first harness missed it because it asserted the concurrent requests COMPLETED rather than that they were PROXIED. Completing and being proxied are different claims. Use an AsyncLocalStorage marker instead, so "am I inside the config read?" is answered per call chain rather than per process. Concurrent requests each get their own scope and are all proxied; a fetch issued from within the config load still sees the marker and goes direct, which is the only possible answer there. Not Context (util/context.ts) because that throws when absent by design, and this marker is optional by nature. Verified 12/12 proxied cold and warm. Adversarial pass also covers the input shapes and body kinds the first harness never exercised: URL and Request inputs, a POST body byte-for-byte, a streaming request body, a streamed (SSE) response, an init naming proxy:undefined, and a dead proxy failing closed instead of falling back to a direct send. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Loopback was bypassed as a law, so a target on 127.0.0.1 — an app in Docker, a staging build, something behind an SSH tunnel — could not be tested through the proxy at all, and nothing said why: it simply never appeared in the proxy's history. The reasons behind the law turned out to be conditional, not absolute: "the proxy is on another machine" is usually true, not always, and with Burp on the same host there is nothing to protect that traffic from. So loopback becomes the DEFAULT bypass rather than a rule, and network.proxy.includeLoopback turns it off. Default unchanged. The rule had three consumers and they had already drifted: the predicate behind isBypassed covered 0.0.0.0 and the whole 127.x range, while the child-process NO_PROXY and the browser bypass list each spelled out their own shorter version. 127.0.0.2 was bypassed for replayed requests and proxied in the browser. The two pattern lists are now one constant, so they cannot disagree again. 127.x beyond 127.0.0.1 stays out of it deliberately — NO_PROXY has no range syntax, and a rule only half the consumers can express is the drift this replaces. isBypassed takes the flag as a parameter instead of reading config, so it stays pure and the config is still read in exactly one place. All-or-nothing on purpose: turning it on covers CyberStrike's own local traffic too, because the bypass list matches hosts and cannot separate the target's port from the server's. Stated in the setting's description rather than papered over with a carve-out — it only makes sense with the proxy on this machine, which is the case that motivated it. Verified that all three consumers move together (12 cases): resolver off/false/ true, a replayed request transiting the proxy for real, NO_PROXY emptied, the browser's bypass list emptied, and the browser proxying a localhost page ON THE WIRE (0 hits by default, 2 with the flag, page loads either way). A non-loopback host is unaffected in both directions, and the flag is inert with no proxy set. Regression: matrix 23/23, resolver 15/15, matchers 18/18, bypass parity 8/8, attack_script 16/16, transport hook 9/9, suite 1663 pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A proxy in the operator's environment quietly outranked the config. Measured on the runtime: HTTP_PROXY is read at process start, and no per-request value means "go direct" — undefined, "", null and false all fall back to it, "direct://" and "http://" throw, and deleting the variable or setting NO_PROXY at runtime changes nothing. So the config won where it NAMED a proxy and lost where it said to bypass one. Loopback included, and that is the damaging part: the crawler posts every captured request to our local server, so on a machine with HTTP_PROXY exported those POSTs went to the corporate proxy and the crawl captured nothing, with no error pointing anywhere near the proxy. The config should win everywhere. It now does everywhere it can: - The crawler subprocess is spawned with the config's NO_PROXY merged into its environment. A process cannot undo its own captured proxy, but the environment we hand it at spawn IS honoured — measured — and this is the process that builds it. Its real outbound traffic is untouched: the browser gets the proxy from Playwright and the model calls from their descriptor, both explicit. - childEnv now distinguishes "no proxy configured" from "proxy turned off". Silence still leaves the machine's setting alone, which is right when the user said nothing; enabled:false now blanks it, because a script must not use a proxy the config explicitly disabled. - The NO_PROXY list is built once and shared by both, rather than a third copy. What remains cannot be fixed from inside this process — for a host the config bypasses, the shell's proxy wins — so it is stated once at startup instead of being left as a guarantee that does not hold, naming the proxy and how to resolve it. Verified (10 cases): a child with the raw environment does send loopback to the ambient proxy, and with ours it goes direct — with a proxy configured, with none configured, and with a bypass entry, which also reaches the child. includeLoopback still wins where the operator asked for loopback to be proxied. enabled:false blanks the ambient proxy and the script really goes direct. Saying nothing adds no variables at all. Regression: attack_script 16/16, loopback 12/12, resolver 15/15, transport hook 9/9 + 9/9 adversarial, suite 1663 pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Outbound proxy configuration, second round. The first round shipped the config surface and routed target traffic; this closes the three gaps found afterwards. - CyberStrike's own outbound traffic (model calls, the OAuth refreshes that keep them alive, device flows, llmhook) now follows the proxy under one setting, applied by a single hook on the process's fetch rather than a helper each call site has to remember. Renamed includeProviders to includeInternal to match. - network.proxy.includeLoopback lets a target on localhost be tested through the proxy. Off by default; the loopback rule was written in three places that had already drifted and is now one. - A proxy exported in the operator's shell no longer silently outranks the config where it can be enforced: the crawler subprocess is spawned with the config's NO_PROXY, and an explicitly disabled proxy blanks the ambient one. What cannot be enforced from inside the process is warned about once at startup instead of being left as a guarantee that does not hold. Verified on the merged tree: 1663 unit tests, plus ~180 wire-level cases across the proxy matrix, the transport hook and its adversarial pass, includeLoopback across all three consumers, the ambient-proxy handling, attack_script and the crawler worker. The single failing unit test (xai/grok-3 variants) predates this work and is unrelated.
… starts Reported: with a proxy configured and includeInternal on, the TUI chat worked normally and the proxy stayed completely empty. Not one request. The hook was installed at the CLI entry point. The TUI does not do its work in the process that entry point starts — it runs the server inside a Worker (cli/cmd/tui/thread.ts), which is a separate realm with its own globalThis and never executes index.ts. So the TUI went direct while the operator believed everything was proxied. `run` and the other in-process commands were proxied, which is why it looked like the config was being ignored rather than the hook being absent. This is the exact weakness I noted when adding the hook and then failed to check: installing per entry point makes coverage depend on knowing every entry point. There was already a second one. Move the install into InstanceBootstrap, which every runtime path reaches — cli/bootstrap.ts, the TUI worker, the server, and worktree — so the question becomes "did this runtime start?" rather than "did someone remember?". The call at the CLI entry point stays for the few commands (auth, upgrade) that reach the network without bootstrapping a project; the install is idempotent, so the two cannot disagree. Verified by reproducing the failure shape: a real Worker, bootstrapped the way the TUI worker bootstraps, now has its fetch proxied (0 hits before, 1 after) and the request still succeeds. Hook 9/9, adversarial 9/9, final sweep 10/10, resolver 15/15, loopback 12/12, ambient 10/10, attack_script 16/16, suite 1663. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> (cherry picked from commit eec07de)
Contributor
|
Thanks for your contribution! This PR doesn't have a linked issue. All PRs must reference an existing issue. Please:
See CONTRIBUTING.md for details. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Adds the missing test coverage for the outbound proxy/TLS feature from #102: the
networkconfig schema (including secret redaction), theNetworkresolver (forHost/forUrl, bypass matching, per-host client certificates, TLS precedence), and replay routing through a real local proxy.The hackbrowser side of #102 reuses the same
Networklayer throughforBrowser()(covered here: proxy server/bypass, SOCKS+auth refusal, certificate-error handling); the browser itself is exercised end-to-end in the existing crawler tests, so no separate hackbrowser wiring test was added.Fixes #102
Type of change
Security impact
How did you verify it works?
bun test test/network/network.test.tsfrompackages/cyberstrike→ 52 pass;bun turbo typecheckclean. The wire tests spin up local loopback origin + proxy servers, no external network needed.Checklist
bun turbo typecheckpassesSupersedes #109(原PR分支已删,按原提交重建)