From 457cf906d2a68d7fa92de31fa49cbd944f4d6a65 Mon Sep 17 00:00:00 2001 From: Einar Gaustad Hanus Date: Thu, 7 Aug 2025 15:01:25 +0200 Subject: [PATCH 1/6] add authentication method as github app and add support for rulesets --- README.md | 7 +- githound.ps1 | 184 +++++++++++++++++++++++++++++++++++++++++++++++-- githound_.json | 11 +++ 3 files changed, 195 insertions(+), 7 deletions(-) create mode 100644 githound_.json diff --git a/README.md b/README.md index 960aeef..e78bb55 100644 --- a/README.md +++ b/README.md @@ -98,13 +98,16 @@ Once the PAT is created, GitHub will present it to you as shown below. You must . ./github.ps1 ``` -3. Create a GitHub Session using your Personal Access Token. +3. Create a GitHub Session using your Personal Access Token or as a Github App. ```powershell +#Github Session with PAT $session = New-GitHubSession -OrganizationName -Token (Get-Clipboard) +#Github Session with Github App, require powershell 7.0 or later +$session = New-GithubAppSession -OrganizationName -ClientId (Get-Clipboard) -PrivateKeyPath ``` -Note: You must specify the name of your GitHub organziation. For example, this repository is part of the `SpecterOps` organization, so I would specify `SpecterOps` as the argument for the OrganizationName parameter. Additionally, you must specify your Personal Access Token. I find that it is easiest to paste it directly from the clipboard as this is where it will be after you create it or if you save it in a password manager. +Note: You must specify the name of your GitHub organziation. For example, this repository is part of the `SpecterOps` organization, so I would specify `SpecterOps` as the argument for the OrganizationName parameter. Additionally, you must specify your Personal Access Token (or Priv key and Client ID). I find that it is easiest to paste it directly from the clipboard as this is where it will be after you create it or if you save it in a password manager. 4. Run the collection on the specified organization: diff --git a/githound.ps1 b/githound.ps1 index d17bd3a..33c944e 100644 --- a/githound.ps1 +++ b/githound.ps1 @@ -60,6 +60,49 @@ function New-GithubSession { } } +function New-GithubAppSession { + [OutputType('GitHound.Session')] + [CmdletBinding()] + Param( + [Parameter(Position=0, Mandatory = $true)] + [string] + $OrganizationName, + + [Parameter(Position=1, Mandatory = $true)] + [string] + $ClientId, + + [Parameter(Position=2, Mandatory = $false)] + [string] + $PrivateKeyPath = './priv.pem' + ) + + $header = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((ConvertTo-Json -InputObject @{ + alg = "RS256" + typ = "JWT" + }))).TrimEnd('=').Replace('+', '-').Replace('/', '_'); + + $payload = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((ConvertTo-Json -InputObject @{ + iat = [System.DateTimeOffset]::UtcNow.AddSeconds(-30).ToUnixTimeSeconds() + exp = [System.DateTimeOffset]::UtcNow.AddMinutes(5).ToUnixTimeSeconds() + iss = $ClientId + }))).TrimEnd('=').Replace('+', '-').Replace('/', '_'); + + $rsa = [System.Security.Cryptography.RSA]::Create() + $rsa.ImportFromPem((Get-Content $PrivateKeyPath -Raw)) + + $signature = [Convert]::ToBase64String($rsa.SignData([System.Text.Encoding]::UTF8.GetBytes("$header.$payload"), [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1)).TrimEnd('=').Replace('+', '-').Replace('/', '_') + $jwt = "$header.$payload.$signature" + + $presession = New-GithubSession -OrganizationName $OrganizationName -Token $jwt + + $Installation = Invoke-GithubRestMethod -Session $presession -Path "app/installations" + $AccessToken = Invoke-GithubRestMethod -Session $presession -Path "app/installations/$($Installation.id)/access_tokens" -Method 'POST' + + New-GithubSession -OrganizationName $OrganizationName -Token $AccessToken.token + +} + function Invoke-GithubRestMethod { [CmdletBinding()] Param( @@ -80,10 +123,10 @@ function Invoke-GithubRestMethod { try { do { if($LinkHeader) { - $Response = Invoke-WebRequest -Uri "$LinkHeader" -Headers $Session.Headers -Method Get -ErrorAction Stop + $Response = Invoke-WebRequest -Uri "$LinkHeader" -Headers $Session.Headers -Method $Method -ErrorAction Stop } else { Write-Verbose "https://api.github.com/$($Path)" - $Response = Invoke-WebRequest -Uri "$($Session.Uri)$($Path)" -Headers $Session.Headers -Method Get -ErrorAction Stop + $Response = Invoke-WebRequest -Uri "$($Session.Uri)$($Path)" -Headers $Session.Headers -Method $Method -ErrorAction Stop } $Response.Content | ConvertFrom-Json | ForEach-Object { $_ } @@ -401,7 +444,7 @@ function Git-HoundBranch { $list = [System.Collections.Generic.List[pscustomobject]]::new() $nodes = New-Object System.Collections.ArrayList - $edges = New-Object System.Collections.ArrayList + $edges = New-Object System.Collections.ArrayList } process @@ -518,6 +561,137 @@ function Git-HoundBranch } } + if ($branch.protected) { + try { + $Protections = Invoke-GithubRestMethod -Session $Session -Path "repos/$($repo.Properties.full_name)/rules/branches/$($branch.name)" + + $aggregatedRules = @{ + pull_request = @() + deletion = @() + non_fast_forward = @() + required_signatures = @() + code_scanning = @() + other = @() + } + + $uniqueRulesets = @{} + + + foreach ($rule in $Protections) { + $rulesetId = $rule.ruleset_id + $ruleType = $rule.type + + if (-not $uniqueRulesets.ContainsKey($rulesetId)) { + $uniqueRulesets[$rulesetId] = @{ + source = $rule.ruleset_source + source_type = $rule.ruleset_source_type + rules = @() + } + } + $uniqueRulesets[$rulesetId].rules += $rule + + if ($aggregatedRules.ContainsKey($ruleType)) { + $aggregatedRules[$ruleType] += $rule + } else { + $aggregatedRules['other'] += $rule + } + } + + if ($aggregatedRules['pull_request'].Count -gt 0) { + + $maxRequiredReviews = 0 + $requireCodeOwnerReview = $false + $requireLastPushApproval = $false + + foreach ($prRule in $aggregatedRules['pull_request']) { + + if ($prRule.parameters) { + if ($prRule.parameters.required_approving_review_count -gt $maxRequiredReviews) { + $maxRequiredReviews = $prRule.parameters.required_approving_review_count + } + + if ($prRule.parameters.require_code_owner_review -eq $true) { + $requireCodeOwnerReview = $true + } + + if ($prRule.parameters.require_last_push_approval -eq $true) { + $requireLastPushApproval = $true + } + + } + } + + if ($branch.protection.enabled -and $Protections.required_pull_request_reviews) { + + if ($maxRequiredReviews -gt 0){ + $BranchProtectionProperties["protection_required_pull_request_reviews"] = $true + } + if ($maxRequiredReviews -gt $Protections.required_pull_request_reviews.required_approving_review_count){ + $BranchProtectionProperties["protection_required_approving_review_count"] = $maxRequiredReviews + } + if ($requireCodeOwnerReview -and $maxRequiredReviews -gt 0){ + $BranchProtectionProperties["protection_required_pull_request_reviews"] = $true + $BranchProtectionProperties["protection_require_code_owner_review"] = $requireCodeOwnerReview + } + if ($requireLastPushApproval){ + $BranchProtectionProperties["protection_required_pull_request_reviews"] = $true + $BranchProtectionProperties["protection_require_last_push_approval"] = $requireLastPushApproval + } + + } else { + + $BranchProtectionProperties["protection_required_pull_request_reviews"] = $false + + if ($maxRequiredReviews -gt 0){ + $BranchProtectionProperties["protection_required_pull_request_reviews"] = $true + $BranchProtectionProperties["protection_required_approving_review_count"] = $maxRequiredReviews + } + else { + $BranchProtectionProperties["protection_required_approving_review_count"] = 0 + } + if ($requireCodeOwnerReview -and $maxRequiredReviews -gt 0){ + $BranchProtectionProperties["protection_required_pull_request_reviews"] = $true + $BranchProtectionProperties["protection_require_code_owner_review"] = $requireCodeOwnerReview + } + else { + $BranchProtectionProperties["protection_require_code_owner_review"] = $false + } + + if ($requireLastPushApproval){ + $BranchProtectionProperties["protection_required_pull_request_reviews"] = $true + $BranchProtectionProperties["protection_require_last_push_approval"] = $requireLastPushApproval + } + else { + $BranchProtectionProperties["protection_require_last_push_approval"] = $false + } + } + + } + + else { + $BranchProtectionProperties["protection_required_pull_request_reviews"] = $false + } + + if ($aggregatedRules['deletion'].Count -gt 0) { + $BranchProtectionProperties["protection_restrict_deletions"] = $true + } + + if ($aggregatedRules['required_signatures'].Count -gt 0) { + $BranchProtectionProperties["protection_required_signed_commits"] = $true + } + + if ($aggregatedRules['code_scanning'].Count -gt 0) { + $BranchProtectionProperties["protection_code_scanning"] = $true + } + + + } catch { + Write-Warning "Failed to fetch branch rules for '$($branch.name)': $_" + } + } + + $branchHash = [System.BitConverter]::ToString([System.Security.Cryptography.MD5]::Create().ComputeHash([System.Text.Encoding]::UTF8.GetBytes("$($repo.properties.organization_name)+$($repo.properties.full_name)+$($branch.name)"))) -replace '-', '' + $props = [pscustomobject]@{ organization = Normalize-Null $repo.properties.organization_name organization_id = Normalize-Null $repo.properties.organization_id @@ -532,8 +706,8 @@ function Git-HoundBranch $props | Add-Member -MemberType NoteProperty -Name $BranchProtectionProperty.Key -Value $BranchProtectionProperty.Value } - $null = $nodes.Add((New-GitHoundNode -Id $branch.commit.sha -Kind GHBranch -Properties $props)) - $null = $edges.Add((New-GitHoundEdge -Kind GHHasBranch -StartId $repo.id -EndId $branch.commit.sha)) + $null = $nodes.Add((New-GitHoundNode -Id $branchHash -Kind GHBranch -Properties $props)) + $null = $edges.Add((New-GitHoundEdge -Kind GHHasBranch -StartId $repo.id -EndId $branchHash)) } } diff --git a/githound_.json b/githound_.json new file mode 100644 index 0000000..0c87617 --- /dev/null +++ b/githound_.json @@ -0,0 +1,11 @@ +{ + "metadata": { + "source_kind": "GHBase" + }, + "graph": { + "nodes": [ + null + ], + "edges": [] + } +} From f9e4d19976e6c42027147d6e5def59c08408c8d5 Mon Sep 17 00:00:00 2001 From: Einar Gaustad Hanus Date: Thu, 7 Aug 2025 15:05:23 +0200 Subject: [PATCH 2/6] remove comment out source_kind to become compatiable with neo4j backend --- githound.ps1 | 2 +- githound_.json | 11 ----------- 2 files changed, 1 insertion(+), 12 deletions(-) delete mode 100644 githound_.json diff --git a/githound.ps1 b/githound.ps1 index 33c944e..700c5be 100644 --- a/githound.ps1 +++ b/githound.ps1 @@ -1249,7 +1249,7 @@ function Invoke-GitHound $payload = [PSCustomObject]@{ metadata = [PSCustomObject]@{ - source_kind = "GHBase" + #source_kind = "GHBase" } graph = [PSCustomObject]@{ nodes = $nodes.ToArray() diff --git a/githound_.json b/githound_.json deleted file mode 100644 index 0c87617..0000000 --- a/githound_.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "metadata": { - "source_kind": "GHBase" - }, - "graph": { - "nodes": [ - null - ], - "edges": [] - } -} From 48542364598de3bd4a54d61fb30991b1d136593a Mon Sep 17 00:00:00 2001 From: Einar Gaustad Hanus Date: Sun, 10 Aug 2025 17:02:10 +0200 Subject: [PATCH 3/6] Revert "add authentication method as github app and add support for rulesets" This reverts commit 457cf906d2a68d7fa92de31fa49cbd944f4d6a65. --- README.md | 7 +- githound.ps1 | 184 ++------------------------------------------------- 2 files changed, 7 insertions(+), 184 deletions(-) diff --git a/README.md b/README.md index e78bb55..960aeef 100644 --- a/README.md +++ b/README.md @@ -98,16 +98,13 @@ Once the PAT is created, GitHub will present it to you as shown below. You must . ./github.ps1 ``` -3. Create a GitHub Session using your Personal Access Token or as a Github App. +3. Create a GitHub Session using your Personal Access Token. ```powershell -#Github Session with PAT $session = New-GitHubSession -OrganizationName -Token (Get-Clipboard) -#Github Session with Github App, require powershell 7.0 or later -$session = New-GithubAppSession -OrganizationName -ClientId (Get-Clipboard) -PrivateKeyPath ``` -Note: You must specify the name of your GitHub organziation. For example, this repository is part of the `SpecterOps` organization, so I would specify `SpecterOps` as the argument for the OrganizationName parameter. Additionally, you must specify your Personal Access Token (or Priv key and Client ID). I find that it is easiest to paste it directly from the clipboard as this is where it will be after you create it or if you save it in a password manager. +Note: You must specify the name of your GitHub organziation. For example, this repository is part of the `SpecterOps` organization, so I would specify `SpecterOps` as the argument for the OrganizationName parameter. Additionally, you must specify your Personal Access Token. I find that it is easiest to paste it directly from the clipboard as this is where it will be after you create it or if you save it in a password manager. 4. Run the collection on the specified organization: diff --git a/githound.ps1 b/githound.ps1 index 700c5be..e1f2023 100644 --- a/githound.ps1 +++ b/githound.ps1 @@ -60,49 +60,6 @@ function New-GithubSession { } } -function New-GithubAppSession { - [OutputType('GitHound.Session')] - [CmdletBinding()] - Param( - [Parameter(Position=0, Mandatory = $true)] - [string] - $OrganizationName, - - [Parameter(Position=1, Mandatory = $true)] - [string] - $ClientId, - - [Parameter(Position=2, Mandatory = $false)] - [string] - $PrivateKeyPath = './priv.pem' - ) - - $header = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((ConvertTo-Json -InputObject @{ - alg = "RS256" - typ = "JWT" - }))).TrimEnd('=').Replace('+', '-').Replace('/', '_'); - - $payload = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((ConvertTo-Json -InputObject @{ - iat = [System.DateTimeOffset]::UtcNow.AddSeconds(-30).ToUnixTimeSeconds() - exp = [System.DateTimeOffset]::UtcNow.AddMinutes(5).ToUnixTimeSeconds() - iss = $ClientId - }))).TrimEnd('=').Replace('+', '-').Replace('/', '_'); - - $rsa = [System.Security.Cryptography.RSA]::Create() - $rsa.ImportFromPem((Get-Content $PrivateKeyPath -Raw)) - - $signature = [Convert]::ToBase64String($rsa.SignData([System.Text.Encoding]::UTF8.GetBytes("$header.$payload"), [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1)).TrimEnd('=').Replace('+', '-').Replace('/', '_') - $jwt = "$header.$payload.$signature" - - $presession = New-GithubSession -OrganizationName $OrganizationName -Token $jwt - - $Installation = Invoke-GithubRestMethod -Session $presession -Path "app/installations" - $AccessToken = Invoke-GithubRestMethod -Session $presession -Path "app/installations/$($Installation.id)/access_tokens" -Method 'POST' - - New-GithubSession -OrganizationName $OrganizationName -Token $AccessToken.token - -} - function Invoke-GithubRestMethod { [CmdletBinding()] Param( @@ -123,10 +80,10 @@ function Invoke-GithubRestMethod { try { do { if($LinkHeader) { - $Response = Invoke-WebRequest -Uri "$LinkHeader" -Headers $Session.Headers -Method $Method -ErrorAction Stop + $Response = Invoke-WebRequest -Uri "$LinkHeader" -Headers $Session.Headers -Method Get -ErrorAction Stop } else { Write-Verbose "https://api.github.com/$($Path)" - $Response = Invoke-WebRequest -Uri "$($Session.Uri)$($Path)" -Headers $Session.Headers -Method $Method -ErrorAction Stop + $Response = Invoke-WebRequest -Uri "$($Session.Uri)$($Path)" -Headers $Session.Headers -Method Get -ErrorAction Stop } $Response.Content | ConvertFrom-Json | ForEach-Object { $_ } @@ -444,7 +401,7 @@ function Git-HoundBranch { $list = [System.Collections.Generic.List[pscustomobject]]::new() $nodes = New-Object System.Collections.ArrayList - $edges = New-Object System.Collections.ArrayList + $edges = New-Object System.Collections.ArrayList } process @@ -561,137 +518,6 @@ function Git-HoundBranch } } - if ($branch.protected) { - try { - $Protections = Invoke-GithubRestMethod -Session $Session -Path "repos/$($repo.Properties.full_name)/rules/branches/$($branch.name)" - - $aggregatedRules = @{ - pull_request = @() - deletion = @() - non_fast_forward = @() - required_signatures = @() - code_scanning = @() - other = @() - } - - $uniqueRulesets = @{} - - - foreach ($rule in $Protections) { - $rulesetId = $rule.ruleset_id - $ruleType = $rule.type - - if (-not $uniqueRulesets.ContainsKey($rulesetId)) { - $uniqueRulesets[$rulesetId] = @{ - source = $rule.ruleset_source - source_type = $rule.ruleset_source_type - rules = @() - } - } - $uniqueRulesets[$rulesetId].rules += $rule - - if ($aggregatedRules.ContainsKey($ruleType)) { - $aggregatedRules[$ruleType] += $rule - } else { - $aggregatedRules['other'] += $rule - } - } - - if ($aggregatedRules['pull_request'].Count -gt 0) { - - $maxRequiredReviews = 0 - $requireCodeOwnerReview = $false - $requireLastPushApproval = $false - - foreach ($prRule in $aggregatedRules['pull_request']) { - - if ($prRule.parameters) { - if ($prRule.parameters.required_approving_review_count -gt $maxRequiredReviews) { - $maxRequiredReviews = $prRule.parameters.required_approving_review_count - } - - if ($prRule.parameters.require_code_owner_review -eq $true) { - $requireCodeOwnerReview = $true - } - - if ($prRule.parameters.require_last_push_approval -eq $true) { - $requireLastPushApproval = $true - } - - } - } - - if ($branch.protection.enabled -and $Protections.required_pull_request_reviews) { - - if ($maxRequiredReviews -gt 0){ - $BranchProtectionProperties["protection_required_pull_request_reviews"] = $true - } - if ($maxRequiredReviews -gt $Protections.required_pull_request_reviews.required_approving_review_count){ - $BranchProtectionProperties["protection_required_approving_review_count"] = $maxRequiredReviews - } - if ($requireCodeOwnerReview -and $maxRequiredReviews -gt 0){ - $BranchProtectionProperties["protection_required_pull_request_reviews"] = $true - $BranchProtectionProperties["protection_require_code_owner_review"] = $requireCodeOwnerReview - } - if ($requireLastPushApproval){ - $BranchProtectionProperties["protection_required_pull_request_reviews"] = $true - $BranchProtectionProperties["protection_require_last_push_approval"] = $requireLastPushApproval - } - - } else { - - $BranchProtectionProperties["protection_required_pull_request_reviews"] = $false - - if ($maxRequiredReviews -gt 0){ - $BranchProtectionProperties["protection_required_pull_request_reviews"] = $true - $BranchProtectionProperties["protection_required_approving_review_count"] = $maxRequiredReviews - } - else { - $BranchProtectionProperties["protection_required_approving_review_count"] = 0 - } - if ($requireCodeOwnerReview -and $maxRequiredReviews -gt 0){ - $BranchProtectionProperties["protection_required_pull_request_reviews"] = $true - $BranchProtectionProperties["protection_require_code_owner_review"] = $requireCodeOwnerReview - } - else { - $BranchProtectionProperties["protection_require_code_owner_review"] = $false - } - - if ($requireLastPushApproval){ - $BranchProtectionProperties["protection_required_pull_request_reviews"] = $true - $BranchProtectionProperties["protection_require_last_push_approval"] = $requireLastPushApproval - } - else { - $BranchProtectionProperties["protection_require_last_push_approval"] = $false - } - } - - } - - else { - $BranchProtectionProperties["protection_required_pull_request_reviews"] = $false - } - - if ($aggregatedRules['deletion'].Count -gt 0) { - $BranchProtectionProperties["protection_restrict_deletions"] = $true - } - - if ($aggregatedRules['required_signatures'].Count -gt 0) { - $BranchProtectionProperties["protection_required_signed_commits"] = $true - } - - if ($aggregatedRules['code_scanning'].Count -gt 0) { - $BranchProtectionProperties["protection_code_scanning"] = $true - } - - - } catch { - Write-Warning "Failed to fetch branch rules for '$($branch.name)': $_" - } - } - - $branchHash = [System.BitConverter]::ToString([System.Security.Cryptography.MD5]::Create().ComputeHash([System.Text.Encoding]::UTF8.GetBytes("$($repo.properties.organization_name)+$($repo.properties.full_name)+$($branch.name)"))) -replace '-', '' - $props = [pscustomobject]@{ organization = Normalize-Null $repo.properties.organization_name organization_id = Normalize-Null $repo.properties.organization_id @@ -706,8 +532,8 @@ function Git-HoundBranch $props | Add-Member -MemberType NoteProperty -Name $BranchProtectionProperty.Key -Value $BranchProtectionProperty.Value } - $null = $nodes.Add((New-GitHoundNode -Id $branchHash -Kind GHBranch -Properties $props)) - $null = $edges.Add((New-GitHoundEdge -Kind GHHasBranch -StartId $repo.id -EndId $branchHash)) + $null = $nodes.Add((New-GitHoundNode -Id $branch.commit.sha -Kind GHBranch -Properties $props)) + $null = $edges.Add((New-GitHoundEdge -Kind GHHasBranch -StartId $repo.id -EndId $branch.commit.sha)) } } From 136859f1787c4d76b65598afbed1d9e0da86c81f Mon Sep 17 00:00:00 2001 From: Einar Gaustad Hanus Date: Mon, 11 Aug 2025 17:45:38 +0200 Subject: [PATCH 4/6] Authentication via Github App --- README.md | 9 ++++++--- githound.ps1 | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ee6f35a..f321174 100644 --- a/README.md +++ b/README.md @@ -95,16 +95,19 @@ Once the PAT is created, GitHub will present it to you as shown below. You must 2. Load `github.ps1` in your current PowerShell session: ```powershell - . ./github.ps1 + . ./githound.ps1 ``` -3. Create a GitHub Session using your Personal Access Token. +3. Create a GitHub Session using your Personal Access Token (PAT) or through a Github App. ```powershell +#Github Session with PAT $session = New-GitHubSession -OrganizationName -Token (Get-Clipboard) +#Github Session with Github App, require powershell 7.0 or later +$session = New-GithubAppSession -OrganizationName -ClientId (Get-Clipboard) -PrivateKeyPath ``` -Note: You must specify the name of your GitHub organziation. For example, this repository is part of the `SpecterOps` organization, so I would specify `SpecterOps` as the argument for the OrganizationName parameter. Additionally, you must specify your Personal Access Token. I find that it is easiest to paste it directly from the clipboard as this is where it will be after you create it or if you save it in a password manager. +Note: You must specify the name of your GitHub organziation. For example, this repository is part of the `SpecterOps` organization, so I would specify `SpecterOps` as the argument for the OrganizationName parameter. Additionally, you must specify your Personal Access Token (or Priv key and Client ID). I find that it is easiest to paste it directly from the clipboard as this is where it will be after you create it or if you save it in a password manager. 4. Run the collection on the specified organization: diff --git a/githound.ps1 b/githound.ps1 index 0183cf8..0187fe3 100644 --- a/githound.ps1 +++ b/githound.ps1 @@ -59,6 +59,48 @@ function New-GithubSession { OrganizationName = $OrganizationName } } +function New-GithubAppSession { + [OutputType('GitHound.Session')] + [CmdletBinding()] + Param( + [Parameter(Position=0, Mandatory = $true)] + [string] + $OrganizationName, + + [Parameter(Position=1, Mandatory = $true)] + [string] + $ClientId, + + [Parameter(Position=2, Mandatory = $false)] + [string] + $PrivateKeyPath = './priv.pem' + ) + + $header = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((ConvertTo-Json -InputObject @{ + alg = "RS256" + typ = "JWT" + }))).TrimEnd('=').Replace('+', '-').Replace('/', '_'); + + $payload = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((ConvertTo-Json -InputObject @{ + iat = [System.DateTimeOffset]::UtcNow.AddSeconds(-30).ToUnixTimeSeconds() + exp = [System.DateTimeOffset]::UtcNow.AddMinutes(5).ToUnixTimeSeconds() + iss = $ClientId + }))).TrimEnd('=').Replace('+', '-').Replace('/', '_'); + + $rsa = [System.Security.Cryptography.RSA]::Create() + $rsa.ImportFromPem((Get-Content $PrivateKeyPath -Raw)) + + $signature = [Convert]::ToBase64String($rsa.SignData([System.Text.Encoding]::UTF8.GetBytes("$header.$payload"), [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1)).TrimEnd('=').Replace('+', '-').Replace('/', '_') + $jwt = "$header.$payload.$signature" + + $presession = New-GithubSession -OrganizationName $OrganizationName -Token $jwt + + $Installation = Invoke-GithubRestMethod -Session $presession -Path "app/installations" + $AccessToken = Invoke-GithubRestMethod -Session $presession -Path "app/installations/$($Installation.id)/access_tokens" -Method 'POST' + + New-GithubSession -OrganizationName $OrganizationName -Token $AccessToken.token + +} function Invoke-GithubRestMethod { [CmdletBinding()] @@ -80,10 +122,10 @@ function Invoke-GithubRestMethod { try { do { if($LinkHeader) { - $Response = Invoke-WebRequest -Uri "$LinkHeader" -Headers $Session.Headers -Method Get -ErrorAction Stop + $Response = Invoke-WebRequest -Uri "$LinkHeader" -Headers $Session.Headers -Method $Method -ErrorAction Stop } else { Write-Verbose "https://api.github.com/$($Path)" - $Response = Invoke-WebRequest -Uri "$($Session.Uri)$($Path)" -Headers $Session.Headers -Method Get -ErrorAction Stop + $Response = Invoke-WebRequest -Uri "$($Session.Uri)$($Path)" -Headers $Session.Headers -Method $Method -ErrorAction Stop } $Response.Content | ConvertFrom-Json | ForEach-Object { $_ } From 3c6443e8cdab26ea82e9b42cbb37559ae6b0924e Mon Sep 17 00:00:00 2001 From: Einar Gaustad Hanus Date: Mon, 11 Aug 2025 17:47:40 +0200 Subject: [PATCH 5/6] remove comment of source_kind --- githound.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/githound.ps1 b/githound.ps1 index 0187fe3..252a9c9 100644 --- a/githound.ps1 +++ b/githound.ps1 @@ -1318,7 +1318,7 @@ function Invoke-GitHound Write-Host "[*] Converting to OpenGraph JSON Payload" $payload = [PSCustomObject]@{ metadata = [PSCustomObject]@{ - #source_kind = "GHBase" + source_kind = "GHBase" } graph = [PSCustomObject]@{ nodes = $nodes.ToArray() From 22d5bf2c337db057c0f29857528d569388b10449 Mon Sep 17 00:00:00 2001 From: Einar Gaustad Hanus Date: Mon, 11 Aug 2025 18:15:17 +0200 Subject: [PATCH 6/6] Error handling if more than 1 installation --- githound.ps1 | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/githound.ps1 b/githound.ps1 index 252a9c9..1d258ce 100644 --- a/githound.ps1 +++ b/githound.ps1 @@ -96,6 +96,13 @@ function New-GithubAppSession { $presession = New-GithubSession -OrganizationName $OrganizationName -Token $jwt $Installation = Invoke-GithubRestMethod -Session $presession -Path "app/installations" + + if ($null -eq $Installation -or $Installation.Count -eq 0) { + throw "No installations found for the GitHub App in the organization '$OrganizationName'." + } elseif ($Installation.Count -gt 1) { + throw "Multiple installations found for the GitHub App in the organization '$OrganizationName'. Please specify a single installation." + } + $AccessToken = Invoke-GithubRestMethod -Session $presession -Path "app/installations/$($Installation.id)/access_tokens" -Method 'POST' New-GithubSession -OrganizationName $OrganizationName -Token $AccessToken.token