Skip to content

fix(cli): resolve list/wait projects by repo URL, not directory name (COR-1577) - #137

Merged
juangaitanv merged 5 commits into
mainfrom
juan/cor-1577-resolve-by-repo
Aug 3, 2026
Merged

fix(cli): resolve list/wait projects by repo URL, not directory name (COR-1577)#137
juangaitanv merged 5 commits into
mainfrom
juan/cor-1577-resolve-by-repo

Conversation

@juangaitanv

Copy link
Copy Markdown
Contributor

Fixes COR-1577. Replaces #122, which grew well past the ticket; this is the fix on its own, and the hardening follows in a stacked PR.

The bug

corgea list and standalone corgea wait derived the project from the working-directory basename and sent it as an exact ?project= match. Where the stored name differs — the Bank of Hope case, directory dotnet-azure-web-tsb vs project bohappdev/dotnet-azure-web-tsblist --issues and wait exited 1, and plain list silently printed an empty table.

The fix

Resolve the canonical project from the git remote — GET /api/v1/projects?repo_url=<org/repo>, discovered upward from the CWD so a subdirectory works — then query the listing endpoints by the name the backend actually stores.

Three things make it safe on older and partially-onboarded backends:

  • The path re-check. The backend filters repo_url__icontains, so a query for acme/api also returns acme/api-v2 and the nested mirrors/acme/api. Every candidate is re-checked against the whole post-host path. The same check guards a pre-COR-1426 backend, which ignores the unknown param and returns all projects: none match, so we fall back instead of listing a stranger's scans.
  • The fallback is unchanged behavior. Unconfirmed, the query stays exactly what the pre-COR-1577 CLI sent (determine_project_name), so an old or not-yet-onboarded backend keeps working.
  • Empty is no longer silently fine. An unresolved project with no scans errors and names what was tried; a confirmed project with no scans still exits 0, so CI polling is unaffected.

Escape hatches: --project-name queries an exact name and skips resolution entirely; --repo resolves from a given slug or URL instead of the remote. Both are on list and wait.

Also removes the client-side scan.project == cwd_basename filter — the server already filtered, and that pass discarded every resolved scan.

Deliberately not here

Kept out to hold this to the ticket; all of it lands in the stacked follow-up:

  • pagination of /projects (page 1 only today — see COR-1729, which would let the CLI drop both the walk and the re-check)
  • host disambiguation for the same org/repo on two forges
  • hard-vs-soft failure for malformed /projects envelopes
  • percent-encoding the project name into /issues (pre-existing)
  • --project-id, SCA selector scoping, argument structs

Verification

cargo build, cargo clippy --all-targets (clean), cargo fmt --check, and the full suite under CI=1 GITHUB_ACTIONS=true — 537 tests.

16 new e2e tests cover: the canonical name driving /scans and /issues (asserted on the request the CLI sent, from a checkout named build-123, so a fallback cannot pass); resolution from a subdirectory; --repo; the miss naming the repo with no table; confirmed-empty exiting 0; the legacy fallback name; --project-name skipping resolution; the JSON envelope preceding the miss; --scan-id skipping resolution; and the post-scan corgea scan path resolving nothing.

Size

429 lines of production code (~340 excluding comments and blank lines), 825 of tests.

Comment thread src/utils/api.rs
Comment thread src/utils/api.rs
Comment thread src/utils/api.rs
Comment thread src/list.rs
Comment thread src/list.rs
COR-1577. `corgea list` and standalone `corgea wait` derived the project
from the working-directory basename and sent it as an exact `?project=`
match. Where the stored name differs — the Bank of Hope case, dir
`dotnet-azure-web-tsb` vs project `bohappdev/dotnet-azure-web-tsb` —
`list --issues` and `wait` exited 1 and plain `list` silently printed an
empty table.

Both now resolve the canonical project from the git remote:
`GET /api/v1/projects?repo_url=<org/repo>`, discovered upward from the CWD
so a subdirectory works too, then query the listing endpoints by the name
the backend actually stores.

- The backend filters `repo_url__icontains`, so every candidate is
  re-checked against the whole post-host path. That also guards a
  pre-COR-1426 backend, which ignores the unknown param and returns ALL
  projects: none match, and we fall back rather than list a stranger's
  scans.
- Unconfirmed, the query stays exactly what the pre-COR-1577 CLI sent, so
  an old or not-yet-onboarded backend keeps working.
- `--project-name` queries an exact name and skips resolution entirely;
  `--repo` resolves from a given slug or URL instead of the remote.
- An unresolved project with no scans is now an error naming what was
  tried, instead of an empty table; a confirmed project with no scans
  still exits 0 so CI polling is unaffected.
- The client-side `scan.project == cwd_basename` filter is gone — the
  server already filtered, and that pass discarded every resolved scan.
@juangaitanv
juangaitanv force-pushed the juan/cor-1577-resolve-by-repo branch from 0184a26 to 1a543f4 Compare July 30, 2026 13:33
Comment thread src/wait.rs Outdated
Comment thread src/wait.rs Outdated

@corgea-security corgea-security left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Automated review risk: 4/5.

Five earlier comments are addressed, but two high-priority wait-path defects remain: canonical project links are malformed and explicit scan IDs unnecessarily depend on repository resolution.

Critical or high-priority changes must be addressed.

Automatic approval was not submitted: automated review found critical or high-priority findings.

@corgea-security corgea-security added the dennis-reviewed Dennis completed an automated review label Jul 30, 2026
Comment thread src/wait.rs

@corgea-security corgea-security left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Automated review risk: 4/5.

Core resolution logic is well tested, but two high-priority wait paths remain broken. Earlier resolved comments are addressed by the supplied diff.

Critical or high-priority changes must be addressed.

Automatic approval was not submitted: automated review found critical or high-priority findings.

@juangaitanv
juangaitanv force-pushed the juan/cor-1577-resolve-by-repo branch from db2e750 to eaf745d Compare July 31, 2026 10:35
…resolution when a scan id is given, honest empty-list copy
Comment thread src/utils/api.rs
Comment thread src/utils/api.rs
Comment thread src/utils/api.rs
Comment thread src/list.rs
Comment thread src/list.rs
Comment thread src/utils/api.rs
Comment thread src/utils/api.rs
Comment thread src/utils/api.rs
Comment thread src/utils/api.rs
Comment thread src/utils/api.rs
Comment thread src/utils/api.rs
Comment thread src/list.rs
Comment thread src/list.rs
Comment thread src/wait.rs
Comment thread src/main.rs
Comment thread src/list.rs
Comment thread src/list.rs
Comment thread src/list.rs
Comment thread src/wait.rs
Comment thread src/main.rs
Comment thread src/scan.rs Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No actionable findings.

Verified the merge-base diff at b41f553, including every wait::run call site, the list/wait empty and error paths, repository URL normalization and exact-path matching, the /projects response contract, and the new request-target assertions. The latest commit addresses the prior wait-path findings: explicit scan IDs no longer depend on project resolution, and canonical project names are percent-encoded in result links. All prior automation threads are resolved.

Validation passed locally: ./harness test (556 tests), ./harness lint (Clippy and format), and git diff --check. CI is also green across Rust tests and supported build platforms.

Open in Web View Automation 

Sent by Cursor Automation: pr-flow

Comment thread src/utils/generic.rs
let had_scp_colon = url[..first_slash].contains(':');
// URL forms split host from path on '/', scp-like `git@host:org/repo` on ':'.
let mut segments: Vec<&str> = url.split(['/', ':']).filter(|s| !s.is_empty()).collect();
// segments[0] is the host; an all-digit segment right after it is a port.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: this might be an edge case but it's not always entirely true that segments[1] is a port, for example https://gitlab.com/101/team-alpha/my-repo or git.company.internal/9082/project-x

@leenk7991
leenk7991 self-requested a review August 2, 2026 13:54

@leenk7991 leenk7991 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm, can we make the comments and docs more concise

@juangaitanv
juangaitanv merged commit 15b19ab into main Aug 3, 2026
17 checks passed
juangaitanv added a commit that referenced this pull request Aug 3, 2026
* refactor(cli): pass list/wait arguments as structs

`wait::run` took five positional params, four of them Option<String>;
`repo` and `project_id` were adjacent and same-typed, so transposing them
at the scan.rs call sites compiled silently and changed resolution.
`list::run` took nine and carried #[allow(clippy::too_many_arguments)].

- `ProjectSelector { name, repo }` replaces the two Option<&str> resolver
  params, so the pair travels as one value.
- `WaitArgs`/`ListArgs` name every argument at the call sites; the
  too_many_arguments allow is gone rather than institutionalized.
- list::run now owns its arguments, so two `is_some()` + `unwrap()` pairs
  clippy started flagging become `if let Some(id)`.

* fix(cli): page through /projects, fail closed, disambiguate by host

Three ways the single-page resolver could quietly answer with the wrong
project, all of which end in the legacy-name fallback listing someone
else's scans:

- `/projects` is `@paginated(default_page_size=20, max_page_size=50)` over
  a `repo_url__icontains` filter ordered `-created_at` (doghouse
  api/views/core.py:1640-1683, api/decorators.py:158). Enough `acme/api-*`
  siblings pushed the exact `acme/api` off page 1. Now requests
  page_size=50 and walks until an exact match or the last page; stopping
  early at PROJECTS_MAX_PAGES is an error, not a miss, since the search was
  truncated.
- A 200 whose body does not parse, or which omits `projects` entirely
  (`{"status":"error"}`), read as "no matches". `@paginated` emits the key
  on every 200 including the empty case, so both now fail the parse.
- Two projects can share a path across forges. The host settles it — but as
  a tie-breaker, never a gate: a lone path match is accepted whatever its
  host, so an SSH-config alias origin (`corp-github:org/repo`, the shape
  this repository's own remote uses) still resolves. Several matches with
  none on our host errors and names the competing URLs.

* fix(cli): encode the issues query, add --project-id, scope SCA to the selector

- `get_scan_issues` interpolated the project name straight into the query
  string, so `--project-name 'foo&bar'` sent `?project=foo&bar&page=1` and
  the server read the project as `foo` — returning another project's issues
  under the name the caller asked for. Built with `query` now, like
  `query_scan_list` and `get_sca_issues`.

- `--project-id` exposes the id `wait` already accepted internally from an
  upload response. Paired with a scan id it skips resolution entirely,
  which is what a CI job passing the id between steps wants. clap requires
  the scan id: without one the scan is still picked by the resolved name,
  so a lone --project-id would only relabel the link.

- `--project-name`/`--repo` were accepted with `--sca-issues` and then
  ignored: the SCA branch returns before any resolution and
  `get_sca_issues` sent only pagination, so a script asking for one project
  silently got company-wide findings. `list_sca_issues` does take `project`
  (doghouse api/views/core.py:1179-1195), so the resolved name is threaded
  in. Only an explicit selector scopes it — unflagged `--sca-issues` keeps
  returning the company-wide latest scan, since narrowing that default is a
  separate decision.

* test(cli): share the resolution e2e stub harness

`spawn_stub` was a near-duplicate across list_resolution.rs and
wait_resolution.rs, and the two files hand-rolled the same route matching.

`common::Routes` is one table for every endpoint the two suites stub; an
unset field 404s, so "this endpoint is never dialed" tests just leave it
out. Tests needing an arm outside the table (blocking rules, the scan
upload) match it first and delegate to `Routes::answer`.

Scaffolding only: no assertion changed.

* fix(cli): link the scan's canonical project on the wait scan-id path

* fix(cli): collect host matches instead of returning the first one

Two projects claiming the same host+path now hit the ambiguity error
instead of resolving to whichever the backend listed first. Also trims
review-flagged comments.

---------

Co-authored-by: Test <test@example.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dennis-reviewed Dennis completed an automated review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants