Public/Test-RepoVisibility.ps1

function Test-RepoVisibility {
    [CmdletBinding()]
    [OutputType([PSCustomObject[]])]
    param(
        [Parameter(Mandatory)]
        [ValidatePattern('^[a-zA-Z0-9._-]+$')]
        [string]$Owner,

        [Parameter(Mandatory)]
        [ValidatePattern('^[a-zA-Z0-9._-]+$')]
        [string]$Repo,

        [Parameter(Mandatory)]
        [string]$Token
    )

    $target = "$Owner/$Repo"
    $results = [System.Collections.Generic.List[PSCustomObject]]::new()

    try {
        $repoInfo = Invoke-GitHubApi -Endpoint "repos/$Owner/$Repo" -Token $Token
    }
    catch {
        $msg = $_.Exception.Message
        if ($msg -match '403') {
            $results.Add((Format-FylgyrResult `
                -CheckName 'RepoVisibility' `
                -Status 'Error' `
                -Severity 'Medium' `
                -Resource $target `
                -Detail 'Insufficient permissions to read repository metadata.' `
                -Remediation 'Use a fine-grained token with Metadata:read permission, or a classic token with repo scope.' `
                -Target $target))
            return $results.ToArray()
        }

        $results.Add((Format-FylgyrResult `
            -CheckName 'RepoVisibility' `
            -Status 'Error' `
            -Severity 'Medium' `
            -Resource $target `
            -Detail "Unexpected error reading repository metadata: $($_.Exception.Message)" `
            -Remediation 'Re-run with a valid token and verify network access to api.github.com.' `
            -Target $target))
        return $results.ToArray()
    }

    $visibility = if ($repoInfo.PSObject.Properties['visibility'] -and $repoInfo.visibility) {
        $repoInfo.visibility
    }
    elseif ($repoInfo.private -eq $true) {
        'private'
    }
    else {
        'public'
    }

    # Naming heuristics for repos that probably should not be public
    $internalMarkers = @(
        '-internal$', '^internal-', '[-_]internal[-_]',
        '-private$', '^private-', '[-_]private[-_]',
        '-confidential$', '[-_]confidential[-_]',
        '-secret$', '[-_]secret[-_]',
        '-staging$', '[-_]staging[-_]',
        '-prod$', '^prod-', '[-_]prod[-_]',
        '-proprietary$'
    )

    $matchedMarker = $null
    foreach ($marker in $internalMarkers) {
        if ($Repo -match $marker) {
            $matchedMarker = $marker
            break
        }
    }

    if ($visibility -eq 'public' -and $matchedMarker) {
        $results.Add((Format-FylgyrResult `
            -CheckName 'RepoVisibility' `
            -Status 'Fail' `
            -Severity 'Medium' `
            -Resource $target `
            -Detail "Repository is public but its name matches an internal/private naming pattern ('$matchedMarker'). This is the class of misconfiguration that caused the Toyota T-Connect source-code exposure, where a repository intended for internal use was publicly accessible for five years." `
            -Remediation "Confirm the repository is intentionally public. If not, change visibility to private in Settings > General > Danger Zone, audit the commit history for secrets, and rotate any exposed credentials." `
            -AttackMapping @('toyota-source-exposure') `
            -Target $target))
        return $results.ToArray()
    }

    $detail = if ($visibility -eq 'public') {
        'Repository is public and its name does not match internal/private naming heuristics.'
    }
    elseif ($visibility -eq 'internal') {
        'Repository visibility is internal.'
    }
    else {
        'Repository is private.'
    }

    $results.Add((Format-FylgyrResult `
        -CheckName 'RepoVisibility' `
        -Status 'Pass' `
        -Severity 'Info' `
        -Resource $target `
        -Detail $detail `
        -Remediation 'No action needed.' `
        -Target $target))

    $results.ToArray()
}