From 8782531ddb4331abe2b4c71ad2c8ff62b28e9f64 Mon Sep 17 00:00:00 2001 From: ai-anant Date: Sun, 2 Aug 2026 19:36:21 +0000 Subject: [PATCH] fix: include GitHub Enterprise internal repositories in scans The repository filter in getRepositories() only kept visibility === 'public', so internal repositories (org-owned repos visible to all enterprise members but not public) were silently dropped even when the configured token could see them. The org and user REST paths now accept both 'public' and 'internal' visibility. Unauthenticated scans are unaffected since the API only returns internal repos to authenticated callers with access. Fixes #6 --- CHANGELOG.md | 1 + js/app.js | 2 +- js/github-client.js | 16 ++++++++++------ 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be79c5b..b5f3190 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Feeds page removed from top navigation** (`feeds.html`): the page is retained and fully functional but no longer occupies a slot in the header navbar, reducing nav clutter ahead of the upcoming Insights page. Users can still access it via direct URL. ### Fixed +- **GitHub Enterprise "internal" repositories now included in org/user scans** (`js/github-client.js`, `js/app.js`): the repository filter in `getRepositories()` only kept `visibility === 'public'`, so internal repositories (a visibility level GitHub Enterprise offers for org-owned repos — visible to all enterprise members but not public) were silently dropped even when the configured token could see them. The org and user REST paths now accept both `public` and `internal` visibility; since the GitHub API itself only returns internal repos to authenticated callers with access, unauthenticated scans are unaffected. The "no repositories found" alert on `index.html` was reworded to match. Fetching an SBOM for an internal repo still requires a token with access to that repo (existing `fetchSBOM` 401/403 handling covers the failure case). - **Findings page threw `ReferenceError: Cannot access 'UNPINNABLE_ACTION_RULES' before initialization`** (`js/findings-page.js`): the new rule-classification Set was originally declared as a `const` inside the DOMContentLoaded handler, just before `generateSecurityFindingsHTML`. Function declarations in the same scope hoist their bodies but not their `const` bindings, and the handler's `await loadFindingsData()` call (which transitively calls `generateSecurityFindingsHTML` via the `renderFunction` callback) runs at source-position 184 — well before the `const` at source-position ~294. The function body referenced the binding while it was still in the temporal dead zone, throwing on the first page load. Moved `UNPINNABLE_ACTION_RULES` to module scope (top of `js/findings-page.js`, outside the DOMContentLoaded handler) so the binding is initialised at script-parse time, before any handler runs. Cache-buster bumped on `findings.html`. - **`window.app` now references the live `SBOMPlayApp` instance** ([`js/app.js`](js/app.js)): page scripts such as [`index-page.js`](js/index-page.js) and the demo page can call `window.app` reliably after initialization. - **GitHub analysis now persists version drift, staleness, and EOX on each dependency in the saved blob** (`js/app.js`, `js/insights-aggregator.js`, `insights.html`): the post-SBOM path `runLicenseAndVersionDriftEnrichment` → `fetchVersionDriftData` attached drift to the in-memory `allDependencies` array but never mirrored it onto `sbomProcessor.dependencies`, so the following `exportData()` wrote empty `versionDrift` / `staleness` fields — Insights' Package Age and Version Drift charts showed 0% coverage even after a full scan. The flow now copies nested `staleness` onto each dep, calls `EnrichmentPipeline.syncDriftToProcessor` after drift fetch (matching upload / `runFullEnrichment`), and `syncEOXToProcessor` after EOX fetch. `InsightsAggregator` treats `dep.versionDrift.staleness` as a fallback when top-level `dep.staleness` is missing. Removed the temporary Insights-only IndexedDB backfill from `js/insights-page.js` in favour of correct scan-time persistence. diff --git a/js/app.js b/js/app.js index 9cd9329..bd4190c 100644 --- a/js/app.js +++ b/js/app.js @@ -1643,7 +1643,7 @@ class SBOMPlayApp { const repositories = await this.githubClient.getRepositories(ownerName); if (repositories.length === 0) { - this.showAlert('No public repositories found for this organization or user', 'info'); + this.showAlert('No public or internal repositories found for this organization or user', 'info'); this.finishAnalysis(); return; } diff --git a/js/github-client.js b/js/github-client.js index 2faf3c3..1079a6b 100644 --- a/js/github-client.js +++ b/js/github-client.js @@ -759,13 +759,16 @@ class GitHubClient { if (response.ok) { const repos = await this.getAllPages(url, response); - // Filter: Only include public repos AND repos owned by this organization + // Filter: Only include public/internal repos AND repos owned by this organization. + // GitHub Enterprise orgs can have "internal" repositories (visible to all + // enterprise members but not public). The API only returns internal repos to + // authenticated callers with access, so no extra auth gate is needed here. const filteredRepos = repos.filter(repo => { - const isPublic = repo.visibility === 'public'; + const isIncluded = repo.visibility === 'public' || repo.visibility === 'internal'; const ownerMatch = repo.owner && (repo.owner.login?.toLowerCase() === normalizedOwnerName || repo.full_name?.toLowerCase().startsWith(`${normalizedOwnerName}/`)); - return isPublic && ownerMatch; + return isIncluded && ownerMatch; }); if (filteredRepos.length < repos.length) { @@ -818,13 +821,14 @@ class GitHubClient { if (response.ok) { console.log(`✅ Found user (REST): ${ownerName}`); const repos = await this.getAllPages(url, response); - // Filter: Only include public repos AND repos owned by this user + // Filter: Only include public/internal repos AND repos owned by this user + // (internal visibility is org-only on GitHub Enterprise, included here for parity) const filteredRepos = repos.filter(repo => { - const isPublic = repo.visibility === 'public'; + const isIncluded = repo.visibility === 'public' || repo.visibility === 'internal'; const ownerMatch = repo.owner && (repo.owner.login?.toLowerCase() === normalizedOwnerName || repo.full_name?.toLowerCase().startsWith(`${normalizedOwnerName}/`)); - return isPublic && ownerMatch; + return isIncluded && ownerMatch; }); if (filteredRepos.length < repos.length) {