Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,10 @@ jobs:

- name: Vet
run: go vet ./...

# Runs the unit suite AND the static egress guard
# (internal/httpx/guard_test.go), which fails the build if any package
# outside internal/httpx constructs a raw http.Client / http.NewRequest —
# so un-receipted egress can't merge.
- name: Test
run: go test ./...
4 changes: 3 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ jobs:
run: |
mkdir -p build
VERSION="${GITHUB_REF_NAME#v}"
LDFLAGS="-s -w -X main.Version=${VERSION}"
# Stamp the build SHA into the Exposure Receipt so a receipt names the
# exact binary that produced it (agent_build_sha).
LDFLAGS="-s -w -X main.Version=${VERSION} -X github.com/Quality-Max/qmax-receipt.BuildSHA=${GITHUB_SHA}"
GOOS=darwin GOARCH=amd64 go build -ldflags="${LDFLAGS}" -o build/qmax-code-darwin-amd64 .
GOOS=darwin GOARCH=arm64 go build -ldflags="${LDFLAGS}" -o build/qmax-code-darwin-arm64 .
GOOS=linux GOARCH=amd64 go build -ldflags="${LDFLAGS}" -o build/qmax-code-linux-amd64 .
Expand Down
110 changes: 110 additions & 0 deletions cmd_receipt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package main

import (
"encoding/json"
"fmt"
"os"
"path/filepath"

receipt "github.com/Quality-Max/qmax-receipt"
)

// handleReceiptCommand implements `qmax-code receipt <list|show|verify> [id|latest]`
// — the customer-facing surface for the Exposure Receipt: "here is exactly what
// left your network this session, and it verifies offline." verify is fully
// offline and prints the provenance-not-honesty caveat.
func handleReceiptCommand(args []string) {
initReceiptPaths()

sub := "list"
if len(args) > 0 {
sub = args[0]
}
switch sub {
case "list":
receiptList()
case "show":
receiptShow(argAt(args, 1))
case "verify":
receiptVerify(argAt(args, 1))
default:
fmt.Fprintln(os.Stderr, "Usage: qmax-code receipt <list|show|verify> [id|latest]")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 TESTINGGROUNDED New CLI command module ships without a test

The file cmd_receipt.go implements the qmax-code receipt subcommand with three subcommands (list, show, verify) and helper functions. It contains non-trivial logic for parsing arguments, resolving receipt IDs, loading and displaying receipts, and error handling. The PR adds this as a new customer-facing surface but does not include a sibling test file. Without tests, regressions in argument parsing, error paths, or the 'latest' resolution logic could silently break the command.

Example:

If `mustResolveReceipt` incorrectly handles an empty ID, the command may crash or misbehave.

Suggested fix:

func TestReceiptList_NoReceipts(t *testing.T) {
    // mock receipt.List to return empty slice, capture output, assert message
}
func TestReceiptVerify_InvalidID(t *testing.T) {
    // expect exit code 1 and error message
}

Why this wasn't caught: The source file has no sibling test in this PR.

More Info
  • Threat model: A broken receipt command would undermine user trust in the exposure receipt system, as users rely on it to verify egress.
  • Specific code citations: New file cmd_receipt.go with functions handleReceiptCommand, receiptList, receiptShow, receiptVerify, mustResolveReceipt.
  • Existing protections: No test file exists for this module in the PR.
  • Proposed mitigation: Create cmd_receipt_test.go with unit tests for each subcommand, including edge cases (no receipts, invalid ID, 'latest' resolution, error paths).
  • Alternative mitigations considered: Rely on integration tests elsewhere, but unit tests are more targeted and maintainable.
  • Severity calibration: Score 4 because the command is a user-facing feature critical to the receipt system; missing tests increase risk of regression.
Prompt To Fix With AI
This is a comment left during a code review.
Path: cmd_receipt.go
Line: 31

Comment:
**New CLI command module ships without a test**

The file `cmd_receipt.go` implements the `qmax-code receipt` subcommand with three subcommands (`list`, `show`, `verify`) and helper functions. It contains non-trivial logic for parsing arguments, resolving receipt IDs, loading and displaying receipts, and error handling. The PR adds this as a new customer-facing surface but does not include a sibling test file. Without tests, regressions in argument parsing, error paths, or the 'latest' resolution logic could silently break the command.

Example:
If `mustResolveReceipt` incorrectly handles an empty ID, the command may crash or misbehave.

Threat model:
A broken receipt command would undermine user trust in the exposure receipt system, as users rely on it to verify egress.

Specific code citations:
New file `cmd_receipt.go` with functions `handleReceiptCommand`, `receiptList`, `receiptShow`, `receiptVerify`, `mustResolveReceipt`.

Existing protections:
No test file exists for this module in the PR.

Proposed mitigation:
Create `cmd_receipt_test.go` with unit tests for each subcommand, including edge cases (no receipts, invalid ID, 'latest' resolution, error paths).

Alternative mitigations considered:
Rely on integration tests elsewhere, but unit tests are more targeted and maintainable.

Severity calibration:
Score 4 because the command is a user-facing feature critical to the receipt system; missing tests increase risk of regression.

Suggested fix shape:
func TestReceiptList_NoReceipts(t *testing.T) {
    // mock receipt.List to return empty slice, capture output, assert message
}
func TestReceiptVerify_InvalidID(t *testing.T) {
    // expect exit code 1 and error message
}

Why this wasn't caught:
The source file has no sibling test in this PR.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 TESTINGGROUNDED New CLI command qmax-code receipt ships without unit tests for error paths and edge cases.

The new cmd_receipt.go file implements a customer-facing CLI command with three subcommands (list, show, verify) and handles several error conditions (missing receipts, invalid IDs, load failures). The [TESTING EVIDENCE] block shows siblingMapping:cmd_receipt.go->NONE — no sibling test file was added in this PR. Without unit tests, regressions in argument parsing, error handling, and the latest resolution logic could silently break the user experience.

Add a test file cmd_receipt_test.go that exercises the subcommand dispatch, validates argAt behavior, and asserts correct exit codes and output for error cases like missing receipts, invalid IDs, and load failures.

Example:

Input: `qmax-code receipt verify non-existent-id`
Expected: Prints error message and exits with code 1.
Actual: Could panic or exit with code 0 if error handling regresses.

Suggested fix:

func TestReceiptVerifyMissingId(t *testing.T) {
    // Mock receipt.Load to return error
    // Call handleReceiptCommand with args ["verify", "non-existent"]
    // Assert stderr contains error and os.Exit(1) is called (use os.Exit override)
}

Why this wasn't caught: The sibling mapping shows no test file for cmd_receipt.go in this PR.

More Info
  • Threat model: A user running qmax-code receipt verify latest could receive a misleading success message or crash if the receipt loading logic regresses, undermining trust in the exposure receipt guarantee.
  • Specific code citations: cmd_receipt.go lines 13-30 define subcommand dispatch; lines 32-38 implement argAt; lines 40-110 implement receiptList, receiptShow, receiptVerify, and mustResolveReceipt with multiple error exits.
  • Existing protections: No unit tests exist for this file per the sibling mapping. The CI runs go test but will not catch regressions in this command's behavior.
  • Proposed mitigation: Create cmd_receipt_test.go with table-driven tests for each subcommand, covering success cases, missing receipts, invalid IDs, and load failures. Mock the receipt package functions where appropriate to isolate CLI logic.
  • Alternative mitigations considered: Relying on integration tests elsewhere is insufficient because they may not exercise all error paths of this specific command.
  • Severity calibration: Score 4 because the CLI is a user-facing feature and untested error handling could lead to confusing or broken user experience, though it does not directly affect the security guarantee of the receipts themselves.
Prompt To Fix With AI
This is a comment left during a code review.
Path: cmd_receipt.go
Line: 31

Comment:
**New CLI command `qmax-code receipt` ships without unit tests for error paths and edge cases.**

The new `cmd_receipt.go` file implements a customer-facing CLI command with three subcommands (`list`, `show`, `verify`) and handles several error conditions (missing receipts, invalid IDs, load failures). The [TESTING EVIDENCE] block shows `siblingMapping:cmd_receipt.go->NONE` — no sibling test file was added in this PR. Without unit tests, regressions in argument parsing, error handling, and the `latest` resolution logic could silently break the user experience.

Add a test file `cmd_receipt_test.go` that exercises the subcommand dispatch, validates `argAt` behavior, and asserts correct exit codes and output for error cases like missing receipts, invalid IDs, and load failures.

Example:
Input: `qmax-code receipt verify non-existent-id`
Expected: Prints error message and exits with code 1.
Actual: Could panic or exit with code 0 if error handling regresses.

Threat model:
A user running `qmax-code receipt verify latest` could receive a misleading success message or crash if the receipt loading logic regresses, undermining trust in the exposure receipt guarantee.

Specific code citations:
`cmd_receipt.go` lines 13-30 define subcommand dispatch; lines 32-38 implement `argAt`; lines 40-110 implement `receiptList`, `receiptShow`, `receiptVerify`, and `mustResolveReceipt` with multiple error exits.

Existing protections:
No unit tests exist for this file per the sibling mapping. The CI runs `go test` but will not catch regressions in this command's behavior.

Proposed mitigation:
Create `cmd_receipt_test.go` with table-driven tests for each subcommand, covering success cases, missing receipts, invalid IDs, and load failures. Mock the `receipt` package functions where appropriate to isolate CLI logic.

Alternative mitigations considered:
Relying on integration tests elsewhere is insufficient because they may not exercise all error paths of this specific command.

Severity calibration:
Score 4 because the CLI is a user-facing feature and untested error handling could lead to confusing or broken user experience, though it does not directly affect the security guarantee of the receipts themselves.

Suggested fix shape:
func TestReceiptVerifyMissingId(t *testing.T) {
    // Mock receipt.Load to return error
    // Call handleReceiptCommand with args ["verify", "non-existent"]
    // Assert stderr contains error and os.Exit(1) is called (use os.Exit override)
}

Why this wasn't caught:
The sibling mapping shows no test file for cmd_receipt.go in this PR.

How can I resolve this? If you propose a fix, please make it concise.

os.Exit(1)
}
}

func argAt(args []string, i int) string {
if len(args) > i {
return args[i]
}
return ""
}

func receiptList() {
paths, err := receipt.List()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if len(paths) == 0 {
fmt.Println("No exposure receipts found.")
return
}
fmt.Printf("%-34s %-20s %-8s %s\n", "RUN ID", "KIND", "REQUESTS", "DESTINATIONS")
for _, p := range paths {
r, err := receipt.Load(p)
if err != nil {
continue
}
fmt.Printf("%-34s %-20s %-8d %v\n", r.RunID, r.RunKind, r.Summary.TotalRequests, r.Destinations)
}
}

func receiptShow(idOrLatest string) {
r := mustResolveReceipt(idOrLatest)
data, err := json.MarshalIndent(r, "", " ")
if err != nil {
fmt.Fprintf(os.Stderr, "Error: cannot marshal receipt: %v\n", err)
os.Exit(1)
}
fmt.Println(string(data))
}

func receiptVerify(idOrLatest string) {
r := mustResolveReceipt(idOrLatest)
if err := receipt.Verify(r); err != nil {
fmt.Fprintf(os.Stderr, "✗ INVALID: %v\n", err)
os.Exit(1)
}
fmt.Printf("✓ VALID — run %s (%s), %d request(s) across %v\n",
r.RunID, r.RunKind, r.Summary.TotalRequests, r.Destinations)
fmt.Println("Note: this proves the receipt was produced by this agent's key (provenance),")
fmt.Println("not that the agent disclosed everything. Cross-check against your own egress logs.")
}

func mustResolveReceipt(idOrLatest string) *receipt.Receipt {
dir, err := receipt.ReceiptsDir()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if idOrLatest == "" || idOrLatest == "latest" {
paths, err := receipt.List()
Comment on lines +85 to +92

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 ERRORGROUNDED receipt.List() error is silently discarded, hiding the real failure from the user

In mustResolveReceipt, when receipt.List() returns an error, the code prints a generic "No exposure receipts found." message and exits. The actual error (permission denied, directory missing, I/O error) is never shown. The user cannot distinguish between 'you have no receipts' and 'I cannot read your receipts directory due to a filesystem error,' which makes troubleshooting impossible and could hide a misconfiguration (e.g. wrong HOME or permission issue).

More Info
  • Threat model: A user who has a legitimate receipts directory but hits a transient permission or filesystem error sees the same message as a user with genuinely zero receipts. They may conclude the tool isn't working, blame the feature, or miss a misconfiguration that prevents receipt collection entirely.
  • Specific code citations: cmd_receipt.go lines 88-92: paths, err := receipt.List(); if err != nil || len(paths) == 0 { fmt.Fprintln(os.Stderr, "No exposure receipts found."); os.Exit(1) } — the err variable is checked but never printed.
  • Existing protections: The receiptList() function separately handles the same condition correctly (prints the error before exiting), showing the pattern the codebase intends to use. mustResolveReceipt diverges from this pattern.
  • Proposed mitigation: Split the condition: check err != nil first, print the error, and exit with code 1. Then check len(paths) == 0 and print the 'No receipts' message. This matches the pattern already used in receiptList().
  • Alternative mitigations considered: A single if err != nil || len(paths) == 0 with fmt.Fprintf(os.Stderr, "Error listing receipts: %v\n", err) would print <nil> in the 'no receipts' case, which is confusing. Splitting is cleaner.
  • Severity calibration: Score 3: Not a correctness bug (the program still exits), but a user-facing robustness gap that violates the principle of least surprise and makes real-world troubleshooting harder. The fix is a simple two-line restructure mirroring the already-correct receiptList() in the same file.
Prompt To Fix With AI
This is a comment left during a code review.
Path: cmd_receipt.go
Line: 85-92

Comment:
**receipt.List() error is silently discarded, hiding the real failure from the user**

In `mustResolveReceipt`, when `receipt.List()` returns an error, the code prints a generic "No exposure receipts found." message and exits. The actual error (permission denied, directory missing, I/O error) is never shown. The user cannot distinguish between 'you have no receipts' and 'I cannot read your receipts directory due to a filesystem error,' which makes troubleshooting impossible and could hide a misconfiguration (e.g. wrong `HOME` or permission issue).

Threat model:
A user who has a legitimate receipts directory but hits a transient permission or filesystem error sees the same message as a user with genuinely zero receipts. They may conclude the tool isn't working, blame the feature, or miss a misconfiguration that prevents receipt collection entirely.

Specific code citations:
`cmd_receipt.go` lines 88-92: `paths, err := receipt.List(); if err != nil || len(paths) == 0 { fmt.Fprintln(os.Stderr, "No exposure receipts found."); os.Exit(1) }` — the `err` variable is checked but never printed.

Existing protections:
The `receiptList()` function separately handles the same condition correctly (prints the error before exiting), showing the pattern the codebase intends to use. `mustResolveReceipt` diverges from this pattern.

Proposed mitigation:
Split the condition: check `err != nil` first, print the error, and exit with code 1. Then check `len(paths) == 0` and print the 'No receipts' message. This matches the pattern already used in `receiptList()`.

Alternative mitigations considered:
A single `if err != nil || len(paths) == 0` with `fmt.Fprintf(os.Stderr, "Error listing receipts: %v\n", err)` would print `<nil>` in the 'no receipts' case, which is confusing. Splitting is cleaner.

Severity calibration:
Score 3: Not a correctness bug (the program still exits), but a user-facing robustness gap that violates the principle of least surprise and makes real-world troubleshooting harder. The fix is a simple two-line restructure mirroring the already-correct `receiptList()` in the same file.

How can I resolve this? If you propose a fix, please make it concise.

if err != nil || len(paths) == 0 {
fmt.Fprintln(os.Stderr, "No exposure receipts found.")
os.Exit(1)
}
r, err := receipt.Load(paths[len(paths)-1])
if err != nil {
Comment on lines +85 to +98

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 SECURITYGROUNDED Unvalidated user input used as path component allows reading arbitrary files outside the receipts directory

Flagged by 2 specialists.

Example:

input: qmax-code receipt show ../../../etc/passwd
actual: filepath.Join("/home/user/.qmax-code/receipts", "../../../etc/passwd.json") resolves to "/etc/passwd.json"; receipt.Load attempts to parse it as JSON, producing a confusing error or partial display
expected: tool rejects the ID with "invalid receipt ID: ../../../etc/passwd"

Suggested fix:

if !receiptIDPattern.MatchString(idOrLatest) {
    fmt.Fprintf(os.Stderr, "invalid receipt ID: %s\n", idOrLatest)
    os.Exit(1)
}
r, err := receipt.Load(filepath.Join(dir, idOrLatest+".json"))

Why this wasn't caught: No test exercises mustResolveReceipt with path-traversal inputs.

Detailed reasoning

mustResolveReceipt takes user-controlled idOrLatest and constructs a file path with filepath.Join(dir, idOrLatest+".json"). filepath.Join calls filepath.Clean, which resolves .. components, so an input like ../../../etc/passwd escapes the receipts directory and reads an arbitrary .json file on disk.

While this is a local CLI tool running with the user's own permissions (so there's no privilege escalation), the tool is positioned as security-aware ('Receipts, not promises'). The function should validate that idOrLatest matches the expected receipt-ID format (e.g. UUID pattern) before using it as a path component, and reject unrecognized formats with a clear error instead of silently treating them as file paths.

More Info
  • Threat model: A user passing a crafted ID like ../../.ssh/config could cause the tool to attempt loading a non-receipt file as a receipt, producing confusing parse errors or, in edge cases, leaking file metadata through JSON error messages. The user already has OS-level read access, so the harm is limited to unexpected tool behavior rather than privilege escalation.
  • Specific code citations: cmd_receipt.go line 98: filepath.Join(dir, idOrLatest+".json") — user input inserted directly into the path with no validation. filepath.Join documentation confirms it calls Clean, which resolves .. components.
  • Existing protections: None — idOrLatest is checked only for "" and "latest" (which are handled separately with receipt.List()). All other values are passed directly to filepath.Join.
  • Proposed mitigation: Validate idOrLatest against a receipt-ID format (e.g. regexp.MustCompile(\^[a-f0-9-]+$`)for UUID v4/v7). Reject non-matching inputs withfmt.Fprintf(os.Stderr, "invalid receipt ID: %s\n", idOrLatest)andos.Exit(1)`. This closes the path traversal while also giving clearer feedback for typos.
  • Alternative mitigations considered: filepath.Base(idOrLatest) strips directory components but still allows reading any .json file within the receipts directory — better than nothing but doesn't validate the format. A UUID regex is stronger because it also catches typos like latestt.
  • Severity calibration: Score 4: Exploitation requires the attacker to control the command argument, which is plausible in scripted or automated environments, and the impact is arbitrary file read. Although the tool runs with user permissions (no privilege escalation), the path traversal is a real logic gap in a security-positioned tool, and the fix is cheap.
Prompt To Fix With AI
This is a comment left during a code review.
Path: cmd_receipt.go
Line: 85-98

Comment:
**Unvalidated user input used as path component allows reading arbitrary files outside the receipts directory**

_Flagged by 2 specialists._

`mustResolveReceipt` takes user-controlled `idOrLatest` and constructs a file path with `filepath.Join(dir, idOrLatest+".json")`. `filepath.Join` calls `filepath.Clean`, which resolves `..` components, so an input like `../../../etc/passwd` escapes the receipts directory and reads an arbitrary `.json` file on disk.

While this is a local CLI tool running with the user's own permissions (so there's no privilege escalation), the tool is positioned as security-aware ('Receipts, not promises'). The function should validate that `idOrLatest` matches the expected receipt-ID format (e.g. UUID pattern) before using it as a path component, and reject unrecognized formats with a clear error instead of silently treating them as file paths.

Example:
input: qmax-code receipt show ../../../etc/passwd
actual: filepath.Join("/home/user/.qmax-code/receipts", "../../../etc/passwd.json") resolves to "/etc/passwd.json"; receipt.Load attempts to parse it as JSON, producing a confusing error or partial display
expected: tool rejects the ID with "invalid receipt ID: ../../../etc/passwd"

Threat model:
A user passing a crafted ID like `../../.ssh/config` could cause the tool to attempt loading a non-receipt file as a receipt, producing confusing parse errors or, in edge cases, leaking file metadata through JSON error messages. The user already has OS-level read access, so the harm is limited to unexpected tool behavior rather than privilege escalation.

Specific code citations:
`cmd_receipt.go` line 98: `filepath.Join(dir, idOrLatest+".json")` — user input inserted directly into the path with no validation. `filepath.Join` documentation confirms it calls `Clean`, which resolves `..` components.

Existing protections:
None — `idOrLatest` is checked only for `""` and `"latest"` (which are handled separately with `receipt.List()`). All other values are passed directly to `filepath.Join`.

Proposed mitigation:
Validate `idOrLatest` against a receipt-ID format (e.g. `regexp.MustCompile(\`^[a-f0-9-]+$\`)` for UUID v4/v7). Reject non-matching inputs with `fmt.Fprintf(os.Stderr, "invalid receipt ID: %s\n", idOrLatest)` and `os.Exit(1)`. This closes the path traversal while also giving clearer feedback for typos.

Alternative mitigations considered:
`filepath.Base(idOrLatest)` strips directory components but still allows reading any `.json` file within the receipts directory — better than nothing but doesn't validate the format. A UUID regex is stronger because it also catches typos like `latestt`.

Severity calibration:
Score 4: Exploitation requires the attacker to control the command argument, which is plausible in scripted or automated environments, and the impact is arbitrary file read. Although the tool runs with user permissions (no privilege escalation), the path traversal is a real logic gap in a security-positioned tool, and the fix is cheap.

Suggested fix shape:
if !receiptIDPattern.MatchString(idOrLatest) {
    fmt.Fprintf(os.Stderr, "invalid receipt ID: %s\n", idOrLatest)
    os.Exit(1)
}
r, err := receipt.Load(filepath.Join(dir, idOrLatest+".json"))

Why this wasn't caught:
No test exercises `mustResolveReceipt` with path-traversal inputs.

How can I resolve this? If you propose a fix, please make it concise.

fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)

Check warning on line 100 in cmd_receipt.go

View check run for this annotation

Sigilix / Sigilix

[P1] logic: Path traversal via user-controlled receipt ID in mustResolveReceipt

The mustResolveReceipt function constructs a file path by concatenating a user-supplied idOrLatest string with .json and joining it with the receipts directory. An attacker can supply a path like ../../etc/passwd to read arbitrary files, or use directory traversal to write outside the intended receipts directory. The function should validate that the ID is a simple alphanumeric identifier before using it in a file path. This is a path traversal vulnerability (CWE-22) because the sink (filepath.Join) consumes a user-controlled string as a path component without sanitization. More Info - Threat model: An attacker with access to the qmax-code receipt command (e.g., via a compromised subprocess, script, or user interaction) can read sensitive files outside the receipts directory, potentially leaking secrets or configuration. - Specific code citations: Line 100: receipt.Load(filepath.Join(dir, idOrLatest+".json")) uses idOrLatest directly in the file path. - Existing protections: The function checks for "" and "latest" but does not validate the ID format. No other sanitization or path cleaning is present. - Proposed mitigation: Validate idOrLatest to contain only alphanumeric characters, hyphens, or underscores, and reject any path separators or dots beyond a single extension. - Alternative mitigations considered: Use path.Clean and check that the result is within the receipts directory, but validation is simpler and more robust. - Severity calibration: Score 4 because exploitation requires the attacker to control the command argument, which is plausible in scripted or automated environments, and the impact is arbitrary file read. Example: idOrLatest: "../../../etc/passwd" actual: loads /etc/passwd.json (or /etc/passwd if .json is stripped) expected: reject invalid ID Suggested fix: if !isValidReceiptID(idOrLatest) { return nil, fmt.Errorf("invalid receipt ID") } Line 100 (high, score 4) · cmd_receipt.go:100
}
return r
}
r, err := receipt.Load(filepath.Join(dir, idOrLatest+".json"))
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
return r
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.24.0

require (
github.com/BourgeoisBear/rasterm v1.1.2
github.com/Quality-Max/qmax-receipt v0.1.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/glamour v0.8.0
github.com/charmbracelet/lipgloss v1.1.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/BourgeoisBear/rasterm v1.1.2 h1:hWHZBZ45N366uNSqxWFYBV0y19q8fXRXADhPkoLF4Ss=
github.com/BourgeoisBear/rasterm v1.1.2/go.mod h1:Ifd+To5s/uyUiYx+B4fxhS8lUNwNLSxDBjskmC5pEyw=
github.com/Quality-Max/qmax-receipt v0.1.0 h1:eHFs+2KhmWxldWWyEz1Tu4Oh+FuUOHt1JYG8IXLgUWw=
github.com/Quality-Max/qmax-receipt v0.1.0/go.mod h1:1GaT7lO+l9itgDgcVHUKF54pbMxKz5671f74Q1c3rDg=
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
Expand Down
12 changes: 8 additions & 4 deletions internal/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"time"

"github.com/qualitymax/qmax-code/internal/api"
"github.com/qualitymax/qmax-code/internal/httpx"
"github.com/qualitymax/qmax-code/internal/session"
"github.com/qualitymax/qmax-code/internal/sysutil"
"github.com/qualitymax/qmax-code/internal/tui"
Expand Down Expand Up @@ -118,7 +119,7 @@ func NewAgent(cfg AgentConfig) *Agent {
Cfg: cfg,
History: []api.Message{},
tools: BuildToolDefs(),
client: &http.Client{Timeout: 300 * time.Second},
client: httpx.NewClient(300 * time.Second),
}
}

Expand Down Expand Up @@ -574,8 +575,10 @@ func (a *Agent) callStreamingAPI(term *tui.Terminal, model string) ([]api.Conten
fmt.Printf("[API] Streaming request: %d bytes, %d messages\n", len(data), len(a.History))
}

// Attribute the model on the Exposure Receipt entry for this prompt.
ctx = httpx.WithModel(ctx, model)
resp, err := doWithRetry(ctx, a.client, func() (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, "POST", api.AnthropicMessagesURL, bytes.NewReader(data))
req, err := httpx.NewRequest(ctx, "POST", api.AnthropicMessagesURL, bytes.NewReader(data))
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -778,8 +781,9 @@ func (a *Agent) callAPI() (*api.APIResponse, error) {
fmt.Printf("[API] Request: %d bytes, %d messages\n", len(data), len(a.History))
}

resp, err := doWithRetry(context.Background(), a.client, func() (*http.Request, error) {
req, err := http.NewRequest("POST", api.AnthropicMessagesURL, bytes.NewReader(data))
ctx := httpx.WithModel(context.Background(), a.Cfg.Model)
resp, err := doWithRetry(ctx, a.client, func() (*http.Request, error) {
req, err := httpx.NewRequest(ctx, "POST", api.AnthropicMessagesURL, bytes.NewReader(data))
if err != nil {
return nil, err
}
Expand Down
5 changes: 3 additions & 2 deletions internal/agent/cerebras.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"time"

"github.com/qualitymax/qmax-code/internal/api"
"github.com/qualitymax/qmax-code/internal/httpx"
)

// Cerebras integration — drive the full native agent loop through Cerebras's
Expand Down Expand Up @@ -44,7 +45,7 @@ func NewCerebrasClient(cfg *api.Config) *CerebrasClient {
Model: model,
APIKey: cfg.CerebrasKey,
ReasoningEffort: api.NormalizeCerebrasReasoningEffort(cfg.CerebrasReasoningEffort),
HTTP: &http.Client{Timeout: 300 * time.Second},
HTTP: httpx.NewClient(300 * time.Second),
}
}

Expand Down Expand Up @@ -207,7 +208,7 @@ func (c *CerebrasClient) Chat(ctx context.Context, msgs []oaiMessage, tools []oa
return nil, fmt.Errorf("marshal request: %w", err)
}

req, err := http.NewRequestWithContext(ctx, "POST", c.BaseURL+"/chat/completions", bytes.NewReader(data))
req, err := httpx.NewRequest(httpx.WithModel(ctx, c.Model), "POST", c.BaseURL+"/chat/completions", bytes.NewReader(data))
if err != nil {
return nil, err
}
Expand Down
5 changes: 3 additions & 2 deletions internal/agent/ollama.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"time"

"github.com/qualitymax/qmax-code/internal/api"
"github.com/qualitymax/qmax-code/internal/httpx"
"github.com/qualitymax/qmax-code/internal/tui"
)

Expand Down Expand Up @@ -78,7 +79,7 @@ func NewOllamaClient(cfg *api.Config) *OllamaClient {
BaseURL: strings.TrimRight(cfg.OllamaURL, "/"),
Model: cfg.OllamaModel,
AgentModel: agentModel,
HTTP: &http.Client{Timeout: 120 * time.Second},
HTTP: httpx.NewClient(120 * time.Second),
cooldownSeconds: ollamaCooldownSec,
}
}
Expand Down Expand Up @@ -172,7 +173,7 @@ func (o *OllamaClient) ChatStreaming(ctx context.Context, system string, history
}

reqURL := o.BaseURL + "/v1/chat/completions"
req, err := http.NewRequestWithContext(ctx, "POST", reqURL, bytes.NewReader(data))
req, err := httpx.NewRequest(httpx.WithModel(ctx, o.Model), "POST", reqURL, bytes.NewReader(data))
if err != nil {
return "", err
}
Expand Down
14 changes: 7 additions & 7 deletions internal/agent/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"fmt"
"io"
"math"
"net/http"
"os"
"os/exec"
"path/filepath"
Expand All @@ -17,6 +16,7 @@ import (
"time"

"github.com/qualitymax/qmax-code/internal/api"
"github.com/qualitymax/qmax-code/internal/httpx"
"github.com/qualitymax/qmax-code/internal/security"
"github.com/qualitymax/qmax-code/internal/sysutil"
"github.com/qualitymax/qmax-code/internal/tui"
Expand Down Expand Up @@ -2277,13 +2277,13 @@ func fetchScriptCode(sctx *api.SessionContext, ctx context.Context, scriptID str
}

url := fmt.Sprintf("%s/api/automation/scripts/%s", apiURL, scriptID)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
req, err := httpx.NewRequest(ctx, "GET", url, nil)
if err != nil {
return fmt.Sprintf(`{"error": %q}`, security.RedactSensitive(err.Error()))
}
req.Header.Set("Authorization", "Bearer "+token)

client := &http.Client{Timeout: 30 * time.Second}
client := httpx.NewClient(30 * time.Second)
resp, err := client.Do(req)
if err != nil {
return fmt.Sprintf(`{"error": %q}`, security.RedactSensitive(err.Error()))
Expand Down Expand Up @@ -2311,14 +2311,14 @@ func updateScriptCode(sctx *api.SessionContext, ctx context.Context, scriptID, n
"code": code,
})

req, err := http.NewRequestWithContext(ctx, "PUT", url, bytes.NewReader(payload))
req, err := httpx.NewRequest(ctx, "PUT", url, bytes.NewReader(payload))
if err != nil {
return fmt.Sprintf(`{"error": %q}`, security.RedactSensitive(err.Error()))
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")

client := &http.Client{Timeout: 30 * time.Second}
client := httpx.NewClient(30 * time.Second)
resp, err := client.Do(req)
if err != nil {
return fmt.Sprintf(`{"error": %q}`, security.RedactSensitive(err.Error()))
Expand Down Expand Up @@ -2647,15 +2647,15 @@ func callVisionAnalysis(sctx *api.SessionContext, imageURL, prompt string) strin
}

data, _ := json.Marshal(reqBody)
req, err := http.NewRequest("POST", api.AnthropicMessagesURL, bytes.NewReader(data))
req, err := httpx.NewRequest(httpx.WithModel(context.Background(), api.ModelHaiku), "POST", api.AnthropicMessagesURL, bytes.NewReader(data))
if err != nil {
return jsonError("Failed to create vision request: " + err.Error())
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("x-api-key", apiKey)
req.Header.Set("anthropic-version", api.AnthropicVersion)

client := &http.Client{Timeout: 30 * time.Second}
client := httpx.NewClient(30 * time.Second)
resp, err := client.Do(req)
if err != nil {
return jsonError("Vision API request failed: " + err.Error())
Expand Down
12 changes: 7 additions & 5 deletions internal/api/auth.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package api

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"

"github.com/qualitymax/qmax-code/internal/httpx"
)

// AuthConfig holds QualityMax authentication credentials.
Expand Down Expand Up @@ -116,8 +118,8 @@ func LoginWithAPIKey(apiKey string) (*AuthConfig, error) {
cloudURL := envOr("QUALITYMAX_URL", DefaultCloudURL)

// Validate by calling /api/me
client := &http.Client{Timeout: 10 * time.Second}
req, _ := http.NewRequest("GET", cloudURL+"/api/me", nil)
client := httpx.NewClient(10 * time.Second)
req, _ := httpx.NewRequest(context.Background(), "GET", cloudURL+"/api/me", nil)
req.Header.Set("Authorization", "Bearer "+stripKeyPrefix(apiKey))

resp, err := client.Do(req)
Expand Down Expand Up @@ -156,8 +158,8 @@ func LoginWithAPIKey(apiKey string) (*AuthConfig, error) {

// fetchMe calls /api/me to get user info (email, id) from the API.
func fetchMe(cfg *AuthConfig) map[string]interface{} {
client := &http.Client{Timeout: 5 * time.Second}
req, err := http.NewRequest("GET", cfg.GetCloudURL()+"/api/me", nil)
client := httpx.NewClient(5 * time.Second)
req, err := httpx.NewRequest(context.Background(), "GET", cfg.GetCloudURL()+"/api/me", nil)
if err != nil {
return nil
}
Expand Down
3 changes: 2 additions & 1 deletion internal/api/claude_code_connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"io"
"net/http"

"github.com/qualitymax/qmax-code/internal/httpx"
"github.com/qualitymax/qmax-code/internal/security"
)

Expand All @@ -27,7 +28,7 @@ func (c *APIClient) ConnectClaudeCode(ctx context.Context, authJSON string) (*Cl
return nil, fmt.Errorf("encode Claude Code credentials: %w", err)
}

req, err := http.NewRequestWithContext(
req, err := httpx.NewRequest(
ctx,
http.MethodPost,
c.BaseURL+"/api/integrations/claude-code/connect",
Expand Down
Loading
Loading