# Architecture ExploitSpec is a single local Go CLI. Its supported interface is the executable and the version 1 YAML format; all Go packages below `internal/` are deliberately private implementation details. The design goal is modest and testable: execute a human-authored HTTP security invariant with enough safety and diagnostic structure to use it repeatedly. ## System context ```mermaid flowchart LR A["Author or pentester"] -->|writes and reviews| S[".exploit.yaml"] E["Process environment / secret manager"] -->|runtime values| C["ExploitSpec CLI"] S --> C C -->|authorized HTTP(S) requests| T["Test target(s)"] T -->|HTTP responses| C C -->|text, JSON, or JUnit| R["Local report / stdout"] ``` There is no ExploitSpec server, account, database, hosted registry, analytics endpoint, or telemetry pipeline. Specs and results stay where the user puts them. The only application traffic created by `run` and `calibrate` is the HTTP workflow described by the spec, subject to the machine's standard DNS and proxy configuration. ## Package map ```text cmd/exploitspec `- process entry point internal/cli |- command and flag parsing |- file/report selection `- exit-code policy internal/discovery `- deterministic recursive spec discovery internal/importer |- bounded cURL command and HAR 1.2 parsers |- explicit HAR entry selection |- structural secret replacement `- starter-spec generation internal/spec |- strict YAML decoding |- version 1 data model `- structural and semantic validation internal/template |- environment interpolation |- document/captured variable interpolation `- recursive JSON-value interpolation internal/runner |- host and URL safety policy |- per-actor HTTP clients and cookie jars |- ordered step execution |- assertions and captures `- calibration state internal/report |- human-readable text |- structured JSON `- JUnit XML ``` Dependencies are intentionally small. The current module uses Go's standard library for networking, TLS, cookies, JSON, regular expressions, XML, and flags, plus `gopkg.in/yaml.v3` for strict YAML decoding. ## Command paths ### Validate ```mermaid flowchart LR P["Input paths"] --> D["Discover files"] D --> L["Read <= 1 MiB"] L --> Y["Strict YAML decode"] Y --> V["Semantic validation"] V --> O["List valid specs"] ``` `validate` ends after semantic validation. It does not resolve runtime secrets, open an HTTP client, or send requests. This separation makes it suitable for fast authoring checks, while recognizing that runtime interpolation can still introduce execution-time errors. ### Import cURL ```mermaid flowchart LR C["cURL text"] --> L["Read stdin at most 1 MiB, or accept --command"] L --> P["Parse as data; no shell"] P --> N["Normalize supported HTTP fields"] N --> X["Drop transport headers and replace imported values"] X --> V["Validate generated version 1 document"] V --> W["Create new YAML without overwrite"] ``` The importer is an authoring boundary, not an execution path. Its parser accepts a bounded subset of cURL syntax, rejects control operators, substitutions, file-backed values, unknown options, and multiple URLs, and never invokes cURL. Non-allowlisted headers, all query/form values, and JSON string leaves become environment placeholders; opaque bodies that cannot be parsed structurally are rejected. The generated document passes the ordinary strict validator before it is written. ### Import HAR ```mermaid flowchart LR H["Local HAR bytes"] --> B["Read at most 8 MiB"] B --> J["Reject ambiguous or malformed JSON"] J --> L["List value-free one-based entry summaries"] L --> S["Require explicit --entry selection"] S --> P["Parse one supported request; no replay"] P --> X["Replace origin, path segments, structural names, and string values"] X --> V["Validate generated version 1 document"] V --> W["Create new YAML without overwrite"] ``` HAR is treated as untrusted captured evidence. The parser accepts only HAR 1.2, bounds archive size, entry count, URL, headers, parameters, body size, generated redactions, final spec size, JSON depth, and token count, and rejects duplicate or Unicode simple-case-fold-conflicting JSON object members. URL and `queryString` representations must agree. Duplicate headers, embedded URL credentials, fragments, missing captured body bytes, format controls in structural names, custom header names, and ambiguous body representations fail closed. Transport-controlled headers are dropped with a review warning. Only JSON and URL-encoded form bodies can be imported. Encoded, file-backed, multipart, binary, opaque, and malformed bodies are rejected. The importer never opens an HTTP client. Generated documents use an environment placeholder for the origin, replace every path segment, query/form name and value, JSON object key, JSON string value, and allowlisted header value, and do not add a remote host to `target.allowed_hosts`. Exact lowercase JSON Patch operation names are the only body strings retained. The selected HTTP method is retained only after exact membership in the fixed standard-method allowlist. JSON numbers, booleans, and null are rejected rather than retained or silently changed to strings. Execution therefore requires explicit environment mapping and a runtime `--allow-host` decision. The output remains an ordinary version 1 document and needs no separate recording or schema extension. ### Run ```mermaid flowchart TD L["Load and validate spec"] --> I["Resolve initial variables"] I --> B["Select and resolve base URL"] B --> H["Apply host safety policy"] H --> C["Create one cookie jar per actor"] C --> S["Build next request"] S --> Q["Send HTTP request"] Q --> M["Read bounded response"] M --> A["Evaluate every assertion"] A -->|all pass| K["Apply captures"] K -->|more steps| S K -->|finished| P["Spec passed"] A -->|assertion fails| F["Spec failed; stop its remaining steps"] L -->|invalid| E["Execution error"] I -->|unresolved| E H -->|rejected| E Q -->|network / timeout| E M -->|too large / unreadable| E K -->|capture error| E ``` The runner processes discovered documents sequentially. Within a document, steps are sequential and fail-fast. A failed or errored document does not stop the CLI from running later discovered documents; the aggregate exit code is selected after reporting all document results. ## Runtime state Runtime state exists only for the duration of a command: - a resolver map holds document variables and successful captures; - one cookie jar per actor maintains session isolation; - a shared cloned HTTP transport manages connections; - response bodies are held in memory only up to the configured limit; - result structs hold statuses and diagnostics until report generation. ExploitSpec does not persist cookies, response bodies, captured values, calibration status, or credentials. An explicitly requested report may persist diagnostics; report files are forced to mode `0600`, and the CLI rejects direct or symlinked collisions with input spec files. ### Actor isolation Each actor gets an independent `http.Client` and cookie jar. This allows one spec to model an owner, attacker, administrator, anonymous caller, or other identities without cookies leaking between them. The clients share a transport, which shares connection management but not cookie state. Headers are layered deterministically: ```text defaults < actor < request ``` The last layer wins for a duplicate header name. ### Data flow between steps The resolver starts with document variables. Before the first request it resolves environment expressions and dependencies between those variables. After a step's assertions pass, captures can add or replace resolver values. Later paths, bodies, JSON string leaves and keys, selected assertion values, and headers can consume those values. Captures do not run after a failed assertion. This avoids promoting data from a response that did not satisfy the expected precondition. ## Result model ExploitSpec distinguishes three outcomes: | Status | Meaning | CLI effect | | --- | --- | ---: | | `passed` | Every executed assertion passed and all captures succeeded | exit `0` if no other result differs | | `failed` | A response was received but violated an invariant | exit `1` unless another spec errors | | `error` | The spec could not be safely or correctly executed | exit `2` | Examples of `error` include an unauthorized host, missing environment variable, network failure, timeout, oversized body, invalid JSON needed for a capture, or report-writing failure. A status mismatch is a `failed` result, not an execution error. Report adapters consume the same result structures: - text emphasizes failed steps and assertions; - JSON includes aggregate counts and complete structured results; - JUnit maps executed steps to test cases, failures, and errors. Response bodies and capture values are not placed wholesale in results. Report adapters deliberately avoid compared JSON/header values and redact templated body assertions. Report files should still be treated as sensitive artifacts because they contain execution metadata, source paths, literal assertion text, and diagnostics. ## Calibration Calibration reuses the ordinary runner rather than introducing separate request semantics: ```mermaid flowchart LR V["Known-vulnerable target"] -->|invariant must fail| R["RED"] R --> F["Fixed target"] F -->|invariant must pass| G["GREEN"] G -->|repeat 1-20 times| S["STABLE"] ``` If the vulnerable execution errors or unexpectedly passes, calibration stops. If any fixed execution fails or errors, repetition stops. A successful calibration is a report-time observation only; it does not mutate the YAML or write an attestation. ## Safety boundaries The safety policy is designed to reduce accidental target expansion and common server-side request forgery hazards. It is not a process sandbox. ### Enforced controls 1. The base URL must use HTTP(S), contain no URL credentials, and have a host. 2. Loopback hosts are allowed for local development. 3. Other hosts require exact, case-normalized authorization from the spec or `--allow-host`. 4. Known cloud metadata endpoints `169.254.169.254` and `fd00:ec2::254`, including canonical IPv4-mapped IPv6 forms, are always rejected. IPv6 zone identifiers are rejected. 5. An interpolated request path cannot change the base scheme or host. 6. Every redirect is rechecked, and more than five redirects are rejected. Credential-bearing headers are removed when the origin changes. 7. URL fragments are rejected in base URLs and request paths because fragments are not transmitted in HTTP requests. 8. Requests time out and response bodies are bounded. 9. Report output cannot name or resolve to an input spec and is forced to mode `0600` when written to a file. 10. TLS uses a minimum version of TLS 1.2. Certificate verification remains on unless the operator explicitly uses `--insecure`. ### Outside the boundary The current runner does not: - resolve allowlisted DNS names and pin them to approved IP ranges; - block every private, link-local, or internal network address; - inspect a request's business impact or prohibit mutating methods; - isolate execution in a container or restricted network namespace; - rate-limit requests beyond sequential execution; - provision, refresh, or revoke credentials; - synthesize or clean up target data; - identify or redact every possible secret from arbitrary literal spec text; - override Go's standard proxy environment behavior. Operational safety therefore depends on isolated targets, synthetic data, least-privilege actors, reviewed specs, and explicit authorization. ## Privacy boundary No code path emits product analytics or contacts project infrastructure. The privacy boundary still includes the test environment: - target servers receive the configured HTTP requests and user agent; - DNS resolvers can observe target lookups; - configured HTTP(S) proxies can observe or relay traffic according to TLS and proxy semantics; - terminal logs and report files expose metadata and diagnostic text; - environment variables remain managed by the invoking process and operating system. “No telemetry” means ExploitSpec does not add collection. It does not make the underlying HTTP test invisible to infrastructure that normally handles it. ## Design constraints The present architecture intentionally favors: - a small declarative format over a general programming language; - ordered, reproducible workflows over autonomous exploration; - explicit actors over a global session; - assertions before captures; - fail-fast behavior within a spec; - a local executable over a hosted control plane; - reviewable evidence over inferred vulnerability claims. Features such as browser automation, arbitrary scripts, conditional execution, and remote orchestration would materially change the threat model. They should not be added as convenience flags without a corresponding design and safety review. ## Extending the implementation Changes should preserve a clear path through the layers: 1. add format fields to `internal/spec` with strict validation; 2. implement execution behavior in `internal/runner`; 3. expose user control through `internal/cli` only when necessary; 4. represent outcomes in the shared result model; 5. keep text, JSON, and JUnit reports semantically aligned; 6. update the specification and architecture documentation; 7. add tests for happy paths, rejection paths, and safety boundaries. Because packages under `internal/` are not a public Go API, compatibility is defined primarily by the YAML version, CLI behavior, report schemas, and exit codes. Format-affecting changes require particular care while the project moves toward a stable release.