Skip to content

Commit 9099ac4

Browse files
committed
feat(policies): implement workflow permissions policy evaluator
- Add GetWorkflowPermissionsAsync method to IGitHubService and GitHubService - Implement CorrectWorkflowPermissionsEvaluator with actual policy logic - Add WorkflowPermissionsResponse class for API deserialization - Update documentation with new method and usage examples - Add policy configuration example for workflow permissions - Update CHANGELOG.md with version 0.3 release notes This implements the security policy to ensure repositories have read-only workflow permissions, preventing unnecessary write access in GitHub Actions.
1 parent f8135f7 commit 9099ac4

File tree

7 files changed

+242
-4
lines changed

7 files changed

+242
-4
lines changed
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Implementation Plan: Workflow Permissions Policy Evaluator
2+
3+
## Overview
4+
5+
Implement the `correct_workflow_permissions` policy evaluator to check that repositories have their GitHub Actions default workflow permissions set to "read" (read repository contents and packages permissions) as specified in the PRD.
6+
7+
## Context
8+
9+
According to the PRD (section 3.4, policy #3), the application must verify that repositories have workflow permissions set to 'Read repository contents and packages permissions'. This is a security best practice that prevents workflows from having unnecessary write access.
10+
11+
## Implementation Steps
12+
13+
### 1. Add GitHub API Method for Workflow Permissions
14+
15+
**File**: `10xGitHubPolicies.App/Services/GitHub/IGitHubService.cs`
16+
17+
Add a new method to the interface:
18+
19+
```csharp
20+
Task<string> GetWorkflowPermissionsAsync(long repositoryId);
21+
```
22+
23+
**File**: `10xGitHubPolicies.App/Services/GitHub/GitHubService.cs`
24+
25+
Implement the method using Octokit's REST API client:
26+
27+
```csharp
28+
public async Task<string> GetWorkflowPermissionsAsync(long repositoryId)
29+
{
30+
var client = await GetAuthenticatedClient();
31+
try
32+
{
33+
// Use the GitHub API endpoint: GET /repos/{owner}/{repo}/actions/permissions/workflow
34+
var connection = client.Connection;
35+
var endpoint = new Uri($"repositories/{repositoryId}/actions/permissions/workflow", UriKind.Relative);
36+
var response = await connection.Get<WorkflowPermissionsResponse>(endpoint, null);
37+
return response.Body.DefaultWorkflowPermissions;
38+
}
39+
catch (NotFoundException)
40+
{
41+
_logger.LogWarning("Workflow permissions not found for repository {RepositoryId}. Actions may be disabled.", repositoryId);
42+
return null;
43+
}
44+
}
45+
46+
// Add a private class for deserialization
47+
private class WorkflowPermissionsResponse
48+
{
49+
public string DefaultWorkflowPermissions { get; set; }
50+
public bool CanApprovePullRequestReviews { get; set; }
51+
}
52+
```
53+
54+
### 2. Implement the Policy Evaluator Logic
55+
56+
**File**: `10xGitHubPolicies.App/Services/Policies/Evaluators/CorrectWorkflowPermissionsEvaluator.cs`
57+
58+
Replace the TODO implementation with actual logic:
59+
60+
```csharp
61+
public async Task<PolicyViolation?> EvaluateAsync(Octokit.Repository repository)
62+
{
63+
var permissions = await _githubService.GetWorkflowPermissionsAsync(repository.Id);
64+
65+
// If permissions is null, Actions might be disabled - consider this compliant
66+
if (permissions == null)
67+
{
68+
return null;
69+
}
70+
71+
// Check if permissions are set to "read" (the secure, restrictive setting)
72+
if (permissions != "read")
73+
{
74+
return new PolicyViolation
75+
{
76+
PolicyType = PolicyType
77+
};
78+
}
79+
80+
return null;
81+
}
82+
```
83+
84+
### 3. Update Documentation
85+
86+
**File**: `docs/github-integration.md`
87+
88+
Add the new method to the IGitHubService methods list (around line 16):
89+
90+
```markdown
91+
- `Task<string> GetWorkflowPermissionsAsync(long repositoryId)`: Gets the default workflow permissions for a repository. Returns "read" or "write", or null if Actions are disabled.
92+
```
93+
94+
Add a usage example in the document:
95+
96+
````markdown
97+
### Example: Checking Workflow Permissions
98+
99+
```csharp
100+
public async Task CheckRepositorySecurityAsync(long repositoryId)
101+
{
102+
var permissions = await _gitHubService.GetWorkflowPermissionsAsync(repositoryId);
103+
104+
if (permissions == "write")
105+
{
106+
_logger.LogWarning("Repository {RepoId} has write permissions for workflows", repositoryId);
107+
}
108+
}
109+
````
110+
111+
```
112+
113+
**File**: `docs/policy-evaluation.md`
114+
115+
Update the example configuration (around line 119) to include a complete example for the workflow permissions policy:
116+
117+
```yaml
118+
- name: 'Verify Workflow Permissions'
119+
type: 'correct_workflow_permissions'
120+
action: 'create-issue'
121+
issue_details:
122+
title: 'Security: Workflow permissions should be read-only'
123+
body: 'This repository has GitHub Actions workflow permissions set to write. For security, please change the default workflow permissions to "Read repository contents and packages permissions" in Settings > Actions > General.'
124+
labels: ['policy-violation', 'security']
125+
```
126+
127+
## Key Technical Details
128+
129+
- **GitHub API Endpoint**: `GET /repos/{owner}/{repo}/actions/permissions/workflow`
130+
- **Expected Values**:
131+
- `"read"` - Read-only permissions (compliant)
132+
- `"write"` - Read and write permissions (violation)
133+
- `null` - Actions disabled or permissions not configured (treated as compliant)
134+
- **Octokit Usage**: Uses the low-level `Connection.Get<T>()` method since Octokit.net may not have a high-level wrapper for this endpoint
135+
- **Error Handling**: Returns null for NotFoundException, treating disabled Actions as compliant
136+
137+
## Testing Considerations
138+
139+
After implementation, test with:
140+
141+
1. A repository with read-only workflow permissions (should pass)
142+
2. A repository with write workflow permissions (should fail)
143+
3. A repository with Actions disabled (should pass)
144+
145+
## Dependencies
146+
147+
- Existing `IGitHubService` infrastructure
148+
- Octokit.net library (already installed)
149+
- Entity Framework entities (PolicyViolation)

10xGitHubPolicies.App/Services/GitHub/GitHubService.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,24 @@ public async Task<string> GetFileContentAsync(string repoName, string path)
116116
}
117117
}
118118

119+
public async Task<string> GetWorkflowPermissionsAsync(long repositoryId)
120+
{
121+
var client = await GetAuthenticatedClient();
122+
try
123+
{
124+
// Use the GitHub API endpoint: GET /repos/{owner}/{repo}/actions/permissions/workflow
125+
var connection = client.Connection;
126+
var endpoint = new Uri($"repositories/{repositoryId}/actions/permissions/workflow", UriKind.Relative);
127+
var response = await connection.Get<WorkflowPermissionsResponse>(endpoint, null);
128+
return response.Body.DefaultWorkflowPermissions;
129+
}
130+
catch (NotFoundException)
131+
{
132+
_logger.LogWarning("Workflow permissions not found for repository {RepositoryId}. Actions may be disabled.", repositoryId);
133+
return null;
134+
}
135+
}
136+
119137
private async Task<GitHubClient> GetAuthenticatedClient()
120138
{
121139
var token = await _cache.GetOrCreateAsync(InstallationTokenCacheKey, async entry =>
@@ -154,4 +172,11 @@ private string GetJwt()
154172
var token = tokenHandler.CreateToken(tokenDescriptor);
155173
return tokenHandler.WriteToken(token);
156174
}
175+
}
176+
177+
// Add a private class for deserialization
178+
public class WorkflowPermissionsResponse
179+
{
180+
public string DefaultWorkflowPermissions { get; set; }
181+
public bool CanApprovePullRequestReviews { get; set; }
157182
}

10xGitHubPolicies.App/Services/GitHub/IGitHubService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ public interface IGitHubService
1414
Task ArchiveRepositoryAsync(long repositoryId);
1515
Task<bool> IsUserMemberOfTeamAsync(string userAccessToken, string org, string teamSlug);
1616
Task<string> GetFileContentAsync(string repoName, string path);
17+
Task<string> GetWorkflowPermissionsAsync(long repositoryId);
1718
}

10xGitHubPolicies.App/Services/Policies/Evaluators/CorrectWorkflowPermissionsEvaluator.cs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,23 @@ public CorrectWorkflowPermissionsEvaluator(IGitHubService githubService)
1616

1717
public async Task<PolicyViolation?> EvaluateAsync(Octokit.Repository repository)
1818
{
19-
// TODO: Implement the logic to check workflow permissions.
20-
// This will likely require a new method in IGitHubService to get repository actions settings.
21-
// For now, we'll assume it's always compliant.
22-
await Task.CompletedTask;
19+
var permissions = await _githubService.GetWorkflowPermissionsAsync(repository.Id);
20+
21+
// If permissions is null, Actions might be disabled - consider this compliant
22+
if (permissions == null)
23+
{
24+
return null;
25+
}
26+
27+
// Check if permissions are set to "read" (the secure, restrictive setting)
28+
if (permissions != "read")
29+
{
30+
return new PolicyViolation
31+
{
32+
PolicyType = PolicyType
33+
};
34+
}
35+
2336
return null;
2437
}
2538
}

CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,32 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## 0.3
9+
10+
### Added
11+
- **Workflow Permissions Policy Evaluator**: Implemented `CorrectWorkflowPermissionsEvaluator` to enforce security best practices for GitHub Actions workflow permissions.
12+
- Added `GetWorkflowPermissionsAsync()` method to `IGitHubService` and `GitHubService` to retrieve repository workflow permissions via GitHub API
13+
- Added `WorkflowPermissionsResponse` class for API response deserialization
14+
- Policy checks that repositories have read-only workflow permissions (security compliance)
15+
- Handles cases where GitHub Actions are disabled (treated as compliant)
16+
- **Enhanced GitHub Integration**: Extended GitHub service with workflow permissions API integration
17+
- Uses GitHub API endpoint `GET /repos/{owner}/{repo}/actions/permissions/workflow`
18+
- Proper error handling for repositories with disabled Actions
19+
- Returns "read", "write", or null for disabled Actions
20+
- **Documentation Updates**:
21+
- Updated `docs/github-integration.md` with new `GetWorkflowPermissionsAsync` method documentation and usage examples
22+
- Updated `docs/policy-evaluation.md` with complete workflow permissions policy configuration example
23+
- Added policy violation issue template for workflow permissions violations
24+
25+
### Changed
26+
- **Policy Evaluation**: `CorrectWorkflowPermissionsEvaluator` now implements actual policy logic instead of placeholder TODO
27+
- **Security Enhancement**: Enforces that repositories must have read-only workflow permissions to prevent unnecessary write access in GitHub Actions
28+
29+
### Dependencies
30+
- No new dependencies added (uses existing Octokit.net library)
31+
32+
---
33+
834
## 0.2
935

1036
### Changed

docs/github-integration.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ This document outlines the approach to integrating with the GitHub API using the
1414
- `Task ArchiveRepositoryAsync(long repositoryId)`: Archives a repository.
1515
- `Task<bool> IsUserMemberOfTeamAsync(string userAccessToken, string org, string teamSlug)`: Verifies if a user is a member of a specific GitHub team (used for access control).
1616
- `Task<string> GetFileContentAsync(string repoName, string path)`: Retrieves file content from a repository. Returns Base64-encoded content or `null` if the file doesn't exist.
17+
- `Task<string> GetWorkflowPermissionsAsync(long repositoryId)`: Gets the default workflow permissions for a repository. Returns "read" or "write", or null if Actions are disabled.
1718

1819
### `GitHubService`
1920
- **Purpose**: Implements `IGitHubService` and handles the authentication and caching of the GitHub App installation token.
@@ -141,6 +142,20 @@ public class AccessControlService
141142
}
142143
```
143144

145+
### Example: Checking Workflow Permissions
146+
147+
```csharp
148+
public async Task CheckRepositorySecurityAsync(long repositoryId)
149+
{
150+
var permissions = await _gitHubService.GetWorkflowPermissionsAsync(repositoryId);
151+
152+
if (permissions == "write")
153+
{
154+
_logger.LogWarning("Repository {RepoId} has write permissions for workflows", repositoryId);
155+
}
156+
}
157+
```
158+
144159
## Best Practices
145160

146161
1. **Use Dependency Injection**: Always inject `IGitHubService` rather than creating instances directly.

docs/policy-evaluation.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,15 @@ policies:
122122
title: 'Compliance: Your new policy violation'
123123
body: 'Details about why this is a violation.'
124124
labels: ['policy-violation', 'your-label']
125+
126+
# Example: Workflow Permissions Policy
127+
- name: 'Verify Workflow Permissions'
128+
type: 'correct_workflow_permissions'
129+
action: 'create-issue'
130+
issue_details:
131+
title: 'Security: Workflow permissions should be read-only'
132+
body: 'This repository has GitHub Actions workflow permissions set to write. For security, please change the default workflow permissions to "Read repository contents and packages permissions" in Settings > Actions > General.'
133+
labels: ['policy-violation', 'security']
125134
```
126135
127136
Once these steps are completed, the `ScanningService` will automatically pick up and execute your new policy evaluator during the next scan.

0 commit comments

Comments
 (0)