Skip to content

Commit 96676b4

Browse files
authored
fix: auto-inject GHEC tenant domains into firewall allowlist (#1316)
* Initial plan * fix: auto-inject GHEC tenant domains into firewall allowlist --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 9761e87 commit 96676b4

File tree

2 files changed

+178
-1
lines changed

2 files changed

+178
-1
lines changed

src/cli.test.ts

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Command } from 'commander';
2-
import { parseEnvironmentVariables, parseDomains, parseDomainsFile, escapeShellArg, joinShellArgs, parseVolumeMounts, isValidIPv4, isValidIPv6, parseDnsServers, parseDnsOverHttps, validateAgentImage, isAgentImagePreset, AGENT_IMAGE_PRESETS, processAgentImageOption, processLocalhostKeyword, validateSkipPullWithBuildLocal, validateAllowHostPorts, parseMemoryLimit, validateFormat, validateApiProxyConfig, buildRateLimitConfig, validateRateLimitFlags, hasRateLimitOptions, collectRulesetFile, validateApiTargetInAllowedDomains, DEFAULT_OPENAI_API_TARGET, DEFAULT_ANTHROPIC_API_TARGET, DEFAULT_COPILOT_API_TARGET, emitApiProxyTargetWarnings, formatItem, program, parseAgentTimeout, applyAgentTimeout, handlePredownloadAction, resolveApiTargetsToAllowedDomains, extractGhesDomainsFromEngineApiTarget } from './cli';
2+
import { parseEnvironmentVariables, parseDomains, parseDomainsFile, escapeShellArg, joinShellArgs, parseVolumeMounts, isValidIPv4, isValidIPv6, parseDnsServers, parseDnsOverHttps, validateAgentImage, isAgentImagePreset, AGENT_IMAGE_PRESETS, processAgentImageOption, processLocalhostKeyword, validateSkipPullWithBuildLocal, validateAllowHostPorts, parseMemoryLimit, validateFormat, validateApiProxyConfig, buildRateLimitConfig, validateRateLimitFlags, hasRateLimitOptions, collectRulesetFile, validateApiTargetInAllowedDomains, DEFAULT_OPENAI_API_TARGET, DEFAULT_ANTHROPIC_API_TARGET, DEFAULT_COPILOT_API_TARGET, emitApiProxyTargetWarnings, formatItem, program, parseAgentTimeout, applyAgentTimeout, handlePredownloadAction, resolveApiTargetsToAllowedDomains, extractGhesDomainsFromEngineApiTarget, extractGhecDomainsFromServerUrl } from './cli';
33
import { redactSecrets } from './redact-secrets';
44
import * as fs from 'fs';
55
import * as path from 'path';
@@ -2216,6 +2216,125 @@ describe('cli', () => {
22162216
});
22172217
});
22182218

2219+
describe('extractGhecDomainsFromServerUrl', () => {
2220+
it('should return empty array when no env vars are set', () => {
2221+
const domains = extractGhecDomainsFromServerUrl({});
2222+
expect(domains).toEqual([]);
2223+
});
2224+
2225+
it('should return empty array when GITHUB_SERVER_URL is github.com', () => {
2226+
const domains = extractGhecDomainsFromServerUrl({ GITHUB_SERVER_URL: 'https://github.com' });
2227+
expect(domains).toEqual([]);
2228+
});
2229+
2230+
it('should return empty array for GHES (non-ghe.com) server URL', () => {
2231+
const domains = extractGhecDomainsFromServerUrl({ GITHUB_SERVER_URL: 'https://github.mycompany.com' });
2232+
expect(domains).toEqual([]);
2233+
});
2234+
2235+
it('should extract GHEC tenant domain and API subdomain from GITHUB_SERVER_URL', () => {
2236+
const domains = extractGhecDomainsFromServerUrl({ GITHUB_SERVER_URL: 'https://myorg.ghe.com' });
2237+
expect(domains).toContain('myorg.ghe.com');
2238+
expect(domains).toContain('api.myorg.ghe.com');
2239+
expect(domains).toHaveLength(2);
2240+
});
2241+
2242+
it('should handle GITHUB_SERVER_URL with trailing slash', () => {
2243+
const domains = extractGhecDomainsFromServerUrl({ GITHUB_SERVER_URL: 'https://myorg.ghe.com/' });
2244+
expect(domains).toContain('myorg.ghe.com');
2245+
expect(domains).toContain('api.myorg.ghe.com');
2246+
});
2247+
2248+
it('should handle GITHUB_SERVER_URL with path components', () => {
2249+
const domains = extractGhecDomainsFromServerUrl({ GITHUB_SERVER_URL: 'https://acme.ghe.com/some/path' });
2250+
expect(domains).toContain('acme.ghe.com');
2251+
expect(domains).toContain('api.acme.ghe.com');
2252+
});
2253+
2254+
it('should extract from GITHUB_API_URL for GHEC', () => {
2255+
const domains = extractGhecDomainsFromServerUrl({ GITHUB_API_URL: 'https://api.myorg.ghe.com' });
2256+
expect(domains).toContain('api.myorg.ghe.com');
2257+
});
2258+
2259+
it('should not add GITHUB_API_URL domain if already present from GITHUB_SERVER_URL', () => {
2260+
const domains = extractGhecDomainsFromServerUrl({
2261+
GITHUB_SERVER_URL: 'https://myorg.ghe.com',
2262+
GITHUB_API_URL: 'https://api.myorg.ghe.com',
2263+
});
2264+
expect(domains).toContain('myorg.ghe.com');
2265+
expect(domains).toContain('api.myorg.ghe.com');
2266+
// api.myorg.ghe.com should appear only once
2267+
const apiCount = domains.filter(d => d === 'api.myorg.ghe.com').length;
2268+
expect(apiCount).toBe(1);
2269+
});
2270+
2271+
it('should return empty array when GITHUB_API_URL is api.github.com', () => {
2272+
const domains = extractGhecDomainsFromServerUrl({ GITHUB_API_URL: 'https://api.github.com' });
2273+
expect(domains).toEqual([]);
2274+
});
2275+
2276+
it('should ignore non-ghe.com GITHUB_API_URL', () => {
2277+
const domains = extractGhecDomainsFromServerUrl({ GITHUB_API_URL: 'https://api.github.mycompany.com' });
2278+
expect(domains).toEqual([]);
2279+
});
2280+
2281+
it('should handle invalid GITHUB_SERVER_URL gracefully', () => {
2282+
const domains = extractGhecDomainsFromServerUrl({ GITHUB_SERVER_URL: 'not-a-valid-url' });
2283+
expect(domains).toEqual([]);
2284+
});
2285+
2286+
it('should handle invalid GITHUB_API_URL gracefully', () => {
2287+
const domains = extractGhecDomainsFromServerUrl({ GITHUB_API_URL: 'not-a-valid-url' });
2288+
expect(domains).toEqual([]);
2289+
});
2290+
});
2291+
2292+
describe('resolveApiTargetsToAllowedDomains with GHEC', () => {
2293+
it('should auto-add GHEC domains when GITHUB_SERVER_URL is a ghe.com tenant', () => {
2294+
const domains: string[] = [];
2295+
const env = { GITHUB_SERVER_URL: 'https://myorg.ghe.com' };
2296+
resolveApiTargetsToAllowedDomains({}, domains, env);
2297+
expect(domains).toContain('myorg.ghe.com');
2298+
expect(domains).toContain('api.myorg.ghe.com');
2299+
});
2300+
2301+
it('should not duplicate GHEC domains if already in allowlist', () => {
2302+
const domains: string[] = ['myorg.ghe.com', 'api.myorg.ghe.com'];
2303+
const env = { GITHUB_SERVER_URL: 'https://myorg.ghe.com' };
2304+
resolveApiTargetsToAllowedDomains({}, domains, env);
2305+
const tenantCount = domains.filter(d => d === 'myorg.ghe.com').length;
2306+
const apiCount = domains.filter(d => d === 'api.myorg.ghe.com').length;
2307+
expect(tenantCount).toBe(1);
2308+
expect(apiCount).toBe(1);
2309+
});
2310+
2311+
it('should not add GHEC domains for public github.com', () => {
2312+
const initialLength = 0;
2313+
const domains: string[] = [];
2314+
const env = { GITHUB_SERVER_URL: 'https://github.com' };
2315+
resolveApiTargetsToAllowedDomains({}, domains, env);
2316+
// github.com itself should NOT be auto-added just from GITHUB_SERVER_URL
2317+
expect(domains).not.toContain('github.com');
2318+
expect(domains).not.toContain('api.github.com');
2319+
expect(domains).toHaveLength(initialLength);
2320+
});
2321+
2322+
it('should auto-add GHEC domain from GITHUB_API_URL', () => {
2323+
const domains: string[] = [];
2324+
const env = { GITHUB_API_URL: 'https://api.myorg.ghe.com' };
2325+
resolveApiTargetsToAllowedDomains({}, domains, env);
2326+
expect(domains).toContain('api.myorg.ghe.com');
2327+
});
2328+
2329+
it('should combine GHEC domains with explicit API target', () => {
2330+
const domains: string[] = [];
2331+
const env = { GITHUB_SERVER_URL: 'https://company.ghe.com' };
2332+
resolveApiTargetsToAllowedDomains({ copilotApiTarget: 'api.company.ghe.com' }, domains, env);
2333+
expect(domains).toContain('company.ghe.com');
2334+
expect(domains).toContain('api.company.ghe.com');
2335+
});
2336+
});
2337+
22192338
describe('resolveApiTargetsToAllowedDomains with GHES', () => {
22202339
it('should auto-add GHES domains when ENGINE_API_TARGET is set', () => {
22212340
const domains: string[] = ['github.com'];

src/cli.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,53 @@ export function emitApiProxyTargetWarnings(
378378
}
379379
}
380380

381+
/**
382+
* Extracts GHEC domains from GITHUB_SERVER_URL and GITHUB_API_URL environment variables.
383+
* When GITHUB_SERVER_URL points to a GHEC tenant (*.ghe.com), returns the tenant hostname
384+
* and its API subdomain so they can be auto-added to the firewall allowlist.
385+
*
386+
* @param env - Environment variables (defaults to process.env)
387+
* @returns Array of domains to auto-add to allowlist, or empty array if not GHEC
388+
*/
389+
export function extractGhecDomainsFromServerUrl(
390+
env: Record<string, string | undefined> = process.env
391+
): string[] {
392+
const domains: string[] = [];
393+
394+
// Extract from GITHUB_SERVER_URL (e.g., https://company.ghe.com)
395+
const serverUrl = env['GITHUB_SERVER_URL'];
396+
if (serverUrl) {
397+
try {
398+
const hostname = new URL(serverUrl).hostname;
399+
if (hostname !== 'github.com' && hostname.endsWith('.ghe.com')) {
400+
// GHEC tenant: add the tenant domain and its API subdomain
401+
// e.g., company.ghe.com → company.ghe.com + api.company.ghe.com
402+
domains.push(hostname);
403+
domains.push(`api.${hostname}`);
404+
}
405+
} catch {
406+
// Invalid URL — skip
407+
}
408+
}
409+
410+
// Extract from GITHUB_API_URL (e.g., https://api.company.ghe.com)
411+
const apiUrl = env['GITHUB_API_URL'];
412+
if (apiUrl) {
413+
try {
414+
const hostname = new URL(apiUrl).hostname;
415+
if (hostname !== 'api.github.com' && hostname.endsWith('.ghe.com')) {
416+
if (!domains.includes(hostname)) {
417+
domains.push(hostname);
418+
}
419+
}
420+
} catch {
421+
// Invalid URL — skip
422+
}
423+
}
424+
425+
return domains;
426+
}
427+
381428
/**
382429
* Extracts GHES API domains from engine.api-target environment variable.
383430
* When engine.api-target is set (indicating GHES), returns the GHES hostname,
@@ -463,6 +510,17 @@ export function resolveApiTargetsToAllowedDomains(
463510
apiTargets.push(env['ANTHROPIC_API_TARGET']);
464511
}
465512

513+
// Auto-populate GHEC domains when GITHUB_SERVER_URL points to a *.ghe.com tenant
514+
const ghecDomains = extractGhecDomainsFromServerUrl(env);
515+
if (ghecDomains.length > 0) {
516+
for (const domain of ghecDomains) {
517+
if (!allowedDomains.includes(domain)) {
518+
allowedDomains.push(domain);
519+
}
520+
}
521+
debug(`Auto-added GHEC domains from GITHUB_SERVER_URL/GITHUB_API_URL: ${ghecDomains.join(', ')}`);
522+
}
523+
466524
// Auto-populate GHES domains when engine.api-target is set
467525
const ghesDomains = extractGhesDomainsFromEngineApiTarget(env);
468526
if (ghesDomains.length > 0) {

0 commit comments

Comments
 (0)