Skip to content

feat: scan any public repository by name, anonymously - #67

Merged
bgard68 merged 1 commit into
mainfrom
feat/public-repo-scan
Aug 24, 2026
Merged

feat: scan any public repository by name, anonymously#67
bgard68 merged 1 commit into
mainfrom
feat/public-repo-scan

Conversation

@bgard68

@bgard68 bgard68 commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Type owner/repository — or paste a github.com URL — and get per-file deterministic findings. No signup, no token, no paste. The same fetch path the 564-workflow field scan proved, now a product surface.

Design constraint: the quota

GitHub allows 60 unauthenticated API calls/hour per source address, shared by every visitor this host serves.

  • The directory listing is the only metered call — file bodies come from raw.githubusercontent.com, which is not counted
  • Every outcome is cached, failures included — a typo retried must not drain the hour for everyone (pinned by test: second scan of the same repo, even differently cased, makes zero HTTP requests)
  • Exhaustion is reported as exhaustion (503 + explanation), never as a missing repository

Trust boundary, amended in writing

The auth middleware justified anonymous access with "no outbound call." This endpoint makes one, so the boundary is restated where the old one was: api.github.com and raw.githubusercontent.com only, no credential attached, read-only by construction. Names are validated against GitHub's own shape before they touch a URL (pinned by test: ../etc never becomes a request). Private repositories cannot appear — anonymous requests cannot see them, which is why this needs no allowlist while /api/github does.

Caps are counted, not silent: files beyond 30 per repo or 100k characters are reported as skippedFiles.

The race the new tests caught

The scenario-loading effect's promise could resolve after a tab switch and wipe newer results via resetResults() — a real clobber, milliseconds wide in production, deterministic under test. The effect now cancels on cleanup; a stale response is a no-op.

Verification

  • 218 backend tests (10 new scanner: cache behaviour, quota mapping, hostile names, size caps; 4 new endpoint: status-code mapping incl. anonymous access)
  • 22 frontend tests (3 new: scan by name, pasted URL, problem-detail surfacing)
  • 0 warnings; the repository's own workflow rules pass

Type owner/repository, get per-file deterministic findings. The demo asked a
visitor to paste YAML; this asks for a name and nothing else — the same fetch
path the 564-workflow field scan already proved, now a product surface.

The quota is the design constraint. GitHub allows 60 unauthenticated API calls
per hour per source address, shared by every visitor this host serves. So the
directory listing is the only metered call — file bodies come from
raw.githubusercontent.com, which is not counted — and every outcome is cached,
failures included: a repository that does not exist is an answer worth
remembering, or one stranger retrying a typo drains the hour for everyone.
Exhaustion is reported as exhaustion, not as a missing repository.

The trust boundary widens by one stated step and no more. The middleware's
justification for anonymous access was "no outbound call"; this endpoint makes
one, so the amended boundary is written down where the old one was: only
api.github.com and raw.githubusercontent.com, no credential attached, read-only
by construction. Owner and repository names are validated against GitHub's own
shape before they touch a URL. Private repositories cannot appear — an
anonymous request cannot see them, which is why this needs no allowlist while
the GitHub App integration does.

Limits are counted, not silent: files beyond the 30-file cap or the 100k
character cap are reported as skipped.

The UI grows a third tab beside Simulation and GitHub Sandbox. Its tests caught
a real race on the way in: the scenario-loading effect's promise could resolve
after a tab switch and wipe newer results via resetResults(). The effect now
cancels on cleanup, so a stale response is a no-op.

218 backend tests and 22 frontend tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RuleDiscovery.All(),
new NeverResolvesActionReferenceResolver(),
new GitHubOptions())),
new MemoryCache(new MemoryCacheOptions()),
Comment on lines +201 to +204
HttpResponseMessage response = new(fake.ListingStatus)
{
Content = new StringContent(fake._listingBody, Encoding.UTF8, "application/json")
};

fake.RawRequests.Add(url);
return Task.FromResult(fake._raw.TryGetValue(url, out string? content)
? new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(content) }
fake.RawRequests.Add(url);
return Task.FromResult(fake._raw.TryGetValue(url, out string? content)
? new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(content) }
: new HttpResponseMessage(HttpStatusCode.NotFound));
@bgard68
bgard68 merged commit b6bf950 into main Aug 24, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants