├── .gitattributes ├── .gitignore ├── CODE-OF-CONDUCT.md ├── LICENSE.TXT ├── README.md ├── build.cmd ├── doc ├── PR01.md ├── PR02.md ├── PR03.md ├── PR04.md ├── PR05.md ├── PR06.md ├── PR07.md ├── PR08.md ├── PR09.md ├── PR10.md ├── PR11.md ├── PR12.md ├── PR13.md ├── PR14.md ├── PR15.md ├── PR16.md ├── PR17.md ├── PR18.md ├── PR19.md ├── PR20.md ├── PR21.md ├── PR22.md ├── PR23.md ├── PR24.md ├── PR25.md └── README.md ├── lib ├── Interop.Microsoft.Office.Interop.Excel.dll └── Interop.Microsoft.Office.Interop.Outlook.dll ├── nuget.config ├── policop.cmd └── src ├── Directory.Build.props ├── Directory.Packages.props ├── Microsoft.Csv.Excel ├── ExcelExtensions.cs └── Microsoft.Csv.Excel.csproj ├── Microsoft.Csv ├── CsvDocument.cs ├── CsvDocumentReader.cs ├── CsvDocumentWriter.cs ├── CsvFile.cs ├── CsvLineReader.cs ├── CsvReader.cs ├── CsvSettings.cs ├── CsvTextReader.cs ├── CsvTextWriter.cs ├── CsvWriter.cs └── Microsoft.Csv.csproj ├── Microsoft.DotnetOrg.GitHubCaching ├── CacheLoader.cs ├── CachedAccessReason.cs ├── CachedActionPermissions.cs ├── CachedBranch.cs ├── CachedBranchProtectionRule.cs ├── CachedFile.cs ├── CachedOrg.cs ├── CachedOrgSecret.cs ├── CachedPermission.cs ├── CachedRepo.cs ├── CachedRepoAllowedActions.cs ├── CachedRepoEnvironment.cs ├── CachedRepoProperty.cs ├── CachedRepoSecret.cs ├── CachedSecret.cs ├── CachedTeam.cs ├── CachedTeamAccess.cs ├── CachedTeamMember.cs ├── CachedUser.cs ├── CachedUserAccess.cs ├── CachedWhatIfPermission.cs ├── GitHubArtifact.cs ├── GitHubClientExtensions.cs ├── GitHubClientFactory.cs ├── GitHubCommunityProfile.cs └── Microsoft.DotnetOrg.GitHubCaching.csproj ├── Microsoft.DotnetOrg.Ospo ├── GitHubInfo.cs ├── Microsoft.DotnetOrg.Ospo.csproj ├── MicrosoftInfo.cs ├── OspoClient.cs ├── OspoClientFactory.cs ├── OspoException.cs ├── OspoLink.cs ├── OspoLinkSet.cs └── OspoUnauthorizedException.cs ├── Microsoft.DotnetOrg.Policies.sln ├── Microsoft.DotnetOrg.Policies ├── CachedOrgExtensions.cs ├── CodeOfConduct.cs ├── Microsoft.DotnetOrg.Policies.csproj ├── PolicyAnalysisContext.cs ├── PolicyDescriptor.cs ├── PolicyRule.cs ├── PolicyRunner.cs ├── PolicySeverity.cs ├── PolicyViolation.cs └── Rules │ ├── PR01_MicrosoftEmployeesShouldBeLinked.cs │ ├── PR02_MicrosoftOwnedRepoShouldAtMostGrantTriageToExternals.cs │ ├── PR03_MicrosoftOwnedTeamShouldOnlyContainEmployees.cs │ ├── PR04_MicrosoftTeamShouldBeMarkedAsOwnedByMicrosoft.cs │ ├── PR05_MarkerTeamShouldOnlyGrantReadAccess.cs │ ├── PR06_InactiveReposShouldBeArchived.cs │ ├── PR07_UnusedTeamShouldNotExist.cs │ ├── PR08_TooManyRepoAdmins.cs │ ├── PR09_TooManyTeamMaintainers.cs │ ├── PR10_AdminsShouldBeInTeams.cs │ ├── PR11_ReposShouldHaveSufficientNumberOfAdmins.cs │ ├── PR12_BotsShouldBeInTheBotsTeam.cs │ ├── PR13_CollaboratorAccessIsSuperfluous.cs │ ├── PR14_RepoOwnershipMustBeExplicit.cs │ ├── PR15_RepoMustHaveACodeOfConduct.cs │ ├── PR16_ReposMustLinkCorrectCodeOfConduct.cs │ ├── PR17_TeamsShouldHaveSufficientNumberOfMaintainers.cs │ ├── PR18_ReposShouldNotUseDeprecatedBranchNames.cs │ ├── PR19_DefaultBranchesShouldBeProtected.cs │ ├── PR20_ReleaseBranchesShouldBeProtected.cs │ ├── PR21_MicrosoftOwnedReposShouldNotUseSecrets.cs │ ├── PR22_MicrosoftOwnedReposShouldDisableGitHubActionsWhenNotUsed.cs │ ├── PR23_MicrosoftOwnedReposShouldRestrictGitHubActions.cs │ ├── PR24_MicrosoftOwnedReposShouldNotUseExternalContributorsForRead.cs │ └── PR25_MicrosoftOwnedPrivateReposNotShouldGrantAccessToNonMicrosoftUsers.cs └── policop ├── CacheManager.cs ├── Commands ├── AssignTeamCommand.cs ├── AssignUserCommand.cs ├── AuditActionsCommand.cs ├── AuditCommand.cs ├── AuditLogCommand.cs ├── BlockUserCommand.cs ├── CacheBuildCommand.cs ├── CacheClearCommand.cs ├── CacheExportCommand.cs ├── CacheInfoCommand.cs ├── CacheOrgCommand.cs ├── ChangeVisibilityCommand.cs ├── CheckCommand.cs ├── ContribCommand.cs ├── IssuesCommand.cs ├── ListColumnsCommand.cs ├── ListCommand.cs ├── ListRulesCommand.cs ├── ListTokensCommand.cs ├── MsLookupCommand.cs ├── SetActionPermissionsCommand.cs ├── SetParentTeamCommand.cs ├── StatusCheckCommand.cs ├── WhatIfCommand.cs └── WhoAmICommand.cs ├── OptionSetExtensions.cs ├── Program.cs ├── Properties └── launchSettings.json ├── Reporting ├── OrgReportColumn.cs ├── RepoReportColumn.cs ├── ReportColumn.cs ├── ReportContext.cs ├── ReportRow.cs ├── RowReportColumn.cs ├── TeamAccessReportColumn.cs ├── TeamReportColumn.cs ├── TeamUserReportColumn.cs ├── UserAccessReportColumn.cs └── UserReportColumn.cs ├── TableHelpers.cs ├── ToolCommand.cs └── policop.csproj /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | This project has adopted the code of conduct defined by the Contributor Covenant 4 | to clarify expected behavior in our community. 5 | 6 | For more information, see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct). 7 | -------------------------------------------------------------------------------- /LICENSE.TXT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) .NET Foundation and Contributors 4 | 5 | All rights reserved. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Policy tooling for the dotnet org 2 | 3 | [![Build Status](https://dev.azure.com/dnceng/public/_apis/build/status/dotnet.org-policy?branchName=main)](https://dev.azure.com/dnceng/public/_build/latest?definitionId=650&branchName=main) 4 | 5 | This repo contains tools and tracks policy violations. 6 | 7 | ## Policies and process 8 | 9 | For details on policies, see [the docs](doc/README.md). 10 | 11 | ## Usage 12 | 13 | For the `dotnet` org, the policies are evaluated daily and violations are posted 14 | in the internal repo [org-policy-violations]. The repo is internal because it 15 | contains names of private repos and teams. 16 | 17 | [org-policy-violations]: https://github.com/dotnet/org-policy-violations 18 | 19 | ## Running locally 20 | 21 | You can run the tool locally by cloning this repo and running `policop.cmd` from 22 | the root. 23 | 24 | ### Getting the org data 25 | 26 | Before you can do anything useful, you need to get access to the org data, which 27 | includes repos, teams, users and their relationships. This also includes access 28 | to linking information between Microsoft user accounts and GitHub user accounts. 29 | 30 | Due to performance and API rate limitations it's not practical to query this 31 | information from GitHub when you're experimenting and trying to analyze the org. 32 | So instead, you can download a cached version of the org that was computed and 33 | uploaded to a private Azure DevOps project during the nightly policy runs. 34 | 35 | You do this by running: 36 | 37 | ```PS 38 | $ .\policop cache-build 39 | ``` 40 | 41 | This will download the latest version of the org data and store it on your local 42 | machine. If you run this command for the first time, it will take you to a 43 | website where you'll need to create an access token that the tool will then 44 | store and use on future calls. 45 | 46 | You can check how old your local cache is by running 47 | 48 | ```PS 49 | $ .\policop cache-info 50 | ``` 51 | 52 | You can also clear the cache with 53 | 54 | ```PS 55 | $ .\policop cache-clear -f 56 | ``` 57 | 58 | ### Evaluating policies 59 | 60 | In order to check policies, you simply use this command: 61 | 62 | ```PS 63 | $ .\policop check --excel 64 | ``` 65 | 66 | This will compute all policy violations and display the result in Excel. You can 67 | also write them to a file if you prefer that: 68 | 69 | ```PS 70 | $ .\policop check -o D:\temp\test.csv 71 | ``` 72 | 73 | ### Querying org data 74 | 75 | The primary command is `policop list` which you can use to query information 76 | from the org. 77 | 78 | Using `-r`, `-t`, and `-u` you can list all components of the org: 79 | 80 | * `-r` the list of repos 81 | * `-t` the list of teams 82 | * `-u` the list of users 83 | * `-r -t` the list of repos and permissions teams are given 84 | * `-r -u` the list of repos and permissions users are given 85 | * `-t -u` the list of teams and their members 86 | * `-r -t -u` the list of repos and permissions teams & users are given 87 | 88 | Each of those options accept a list of terms you can use to filter, 89 | with basic wild card support, such as `*core*` or `dotnet*`. 90 | 91 | So to list all teams whose name contains the text `core` you'd do this: 92 | 93 | ```PS 94 | $ .\policop list -t *core* 95 | ``` 96 | 97 | To find all members of all teams named `*core*` you'd do this: 98 | 99 | ```PS 100 | # List team members of teams whose name contains "core" 101 | $ .\policop list -t *core* -u 102 | ``` 103 | 104 | Using `-f` you can also filter: 105 | 106 | ```PS 107 | # List all repos whose name contains dotnet and where a team 108 | # grants admin access 109 | $ .\policop list -r *dotnet* -t -f rt:permission=admin 110 | ``` 111 | 112 | For columns returning `Yes`/`No` you can also use the simple 113 | version: 114 | 115 | ```PS 116 | # List all private repos 117 | $ .\policop list -r -f r:private 118 | ``` 119 | 120 | And lastly, using `-c` you can create custom reports with specific columns: 121 | 122 | ```PS 123 | # List all private repos and show their name, description and list of admins 124 | $ .\policop list -r -f r:private -c r:name r:description r:admins 125 | ``` 126 | 127 | The available columns can be listed by running 128 | 129 | ```PS 130 | $ .\policop list-columns 131 | ``` 132 | 133 | The naming convention indicates when the columns can be used: 134 | 135 | * `r:*` when repos are included 136 | * `t:*` when teams are included 137 | * `u:*` when users are included 138 | * `rt:*` when repos and teams are included 139 | * `ru:*` when repos and users are included 140 | * `tu:*` when teams and users are included 141 | * `rtu:*` when repos, teams, and users are included 142 | 143 | In general, `policop list` will print the results to the console but with `-o` 144 | you can write to a file and with `--excel` you can send it straight into Excel. 145 | -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal 3 | set SLN_FILE=%~dp0src\Microsoft.DotnetOrg.Policies.sln 4 | set OUT_DIR=%~dp0bin\ 5 | dotnet build %SLN_FILE% --nologo --property:OutputPath=%OUT_DIR% -- %* 6 | -------------------------------------------------------------------------------- /doc/PR01.md: -------------------------------------------------------------------------------- 1 | # PR01 Microsoft employees should be linked 2 | 3 | This rule detects users who appear to be Microsoft users (e.g. company or email 4 | field contains "Microsoft") but aren't linked. Linking ensures that: 5 | 6 | 1. We know which Microsoft user account is linked to which GitHub account 7 | 2. We can automatically remove users when they leave the company 8 | 9 | The linking is described in the [documentation] and can be done by individuals 10 | by visiting: 11 | 12 | https://opensource.microsoft.com/link 13 | 14 | [documentation]: (https://docs.opensource.microsoft.com/tools/github/accounts/linking.html) 15 | -------------------------------------------------------------------------------- /doc/PR02.md: -------------------------------------------------------------------------------- 1 | # PR02 Microsoft-owned repo should at most grant 'triage' to externals 2 | 3 | Microsoft-owned repos should at most grant non-Microsoft users with `triage` 4 | permissions. 5 | 6 | * If this is a Microsoft user, they need to [link] their account. 7 | * If this isn't a Microsoft user, their permission needs to be changed to 8 | `triage`. 9 | 10 | [link]: https://docs.opensource.microsoft.com/tools/github/accounts/linking.html 11 | -------------------------------------------------------------------------------- /doc/PR03.md: -------------------------------------------------------------------------------- 1 | # PR03 Microsoft-owned team should only contain Microsoft users 2 | 3 | A team that is owned by Microsoft should only contain Microsoft-users. 4 | 5 | * If this is a Microsoft user, they need to [link] their account. 6 | * If this team is supposed to represent Microsoft and non-Microsoft, the team 7 | shouldn't be owned by Microsoft 8 | * If this isn't a Microsoft user, they need to be removed from this team. 9 | 10 | [link]: https://docs.opensource.microsoft.com/tools/github/accounts/linking.html 11 | -------------------------------------------------------------------------------- /doc/PR04.md: -------------------------------------------------------------------------------- 1 | # PR04 Team should be owned by Microsoft 2 | 3 | A team that is granted more than `triage` permissions to a Microsoft-owned repo 4 | must be owned by Microsoft. 5 | 6 | To mark a team as owned by Microsoft, it needs have the [Microsoft team] as its 7 | (direct or indirect) parent team. 8 | 9 | [Microsoft team]: https://github.com/orgs/dotnet/teams/microsoft 10 | -------------------------------------------------------------------------------- /doc/PR05.md: -------------------------------------------------------------------------------- 1 | # PR05 Marker team should only grant 'read' access 2 | 3 | Marker teams are only used to indicate ownership. It should only ever grant 4 | `read` permissions. Examples of marker teams are: 5 | 6 | * [microsoft](https://github.com/orgs/dotnet/teams/microsoft) 7 | * [microsoft-bots](https://github.com/orgs/dotnet/teams/microsoft-bots) 8 | * [bots](https://github.com/orgs/dotnet/teams/bots) 9 | 10 | Change the permissions for the marker team in the repo to `read`. 11 | -------------------------------------------------------------------------------- /doc/PR06.md: -------------------------------------------------------------------------------- 1 | # PR06 Inactive repos should be archived 2 | 3 | Repos that haven't been pushed to in a long time should be archived. -------------------------------------------------------------------------------- /doc/PR07.md: -------------------------------------------------------------------------------- 1 | # PR07 Unused team should be removed 2 | 3 | A team that isn't assigned to any repos not has any child teams is considered 4 | unused. It should either be used or removed. -------------------------------------------------------------------------------- /doc/PR08.md: -------------------------------------------------------------------------------- 1 | # PR08 Too many repo admins 2 | 3 | Repos should not have more than 10 admins. In case of admin shortage, we always 4 | have org owners that can get people unblocked. -------------------------------------------------------------------------------- /doc/PR09.md: -------------------------------------------------------------------------------- 1 | # PR09 Too many team maintainers 2 | 3 | Teams should not have more than 10 maintainers. In case of admin shortage, we 4 | always have org owners that can get people unblocked. -------------------------------------------------------------------------------- /doc/PR10.md: -------------------------------------------------------------------------------- 1 | # PR10 Admins should be in teams 2 | 3 | Individuals shouldn't be granted admin permissions to repos in the long run. 4 | Rather, they should be organized in teams. -------------------------------------------------------------------------------- /doc/PR11.md: -------------------------------------------------------------------------------- 1 | # PR11 Repos should have a sufficient number of admins 2 | 3 | In order to make sure that a repo can be administered without bottleneck, it 4 | needs a sufficient number of admins. We recommend that each repo has at least 5 | two admins, not counting org owners. -------------------------------------------------------------------------------- /doc/PR12.md: -------------------------------------------------------------------------------- 1 | # PR12 Bots should be in the 'bots' team 2 | 3 | Bots should be known and maintained in a central list. 4 | 5 | * If this is in fact a human, mark this issue as `policy-override` and close the 6 | issue 7 | * If this is a bot, add it to the team `bots` 8 | -------------------------------------------------------------------------------- /doc/PR13.md: -------------------------------------------------------------------------------- 1 | # PR13 Collaborator access is superfluous 2 | 3 | Maintaining collaborators on GitHub can be tedious. To keep things tidy, we 4 | don't want redundant permissions. For example, organization owners and users 5 | that have an equal to higher permission via a team shouldn't be added as 6 | collaborators. 7 | 8 | In those cases, you should remove the collaborator access. -------------------------------------------------------------------------------- /doc/PR14.md: -------------------------------------------------------------------------------- 1 | # PR14 Repo ownership must be explicit 2 | 3 | Repos need to indicate whether they are owned by Microsoft. That's because we 4 | have some policies that are specific to Microsoft repos. This policy ensures 5 | that repos aren't treated as non-owned by Microsoft by default. 6 | 7 | *Note: Some orgs (such as ASP.NET) are assumed to be entirely owned by 8 | Microsoft. This rule only applies to orgs where some repos are owned by 9 | Microsoft and some are owned by the community.* 10 | 11 | * **Owned by Microsoft**. Assign the [microsoft] team with `read` permissions. 12 | * **Not owned by Microsoft**. Assign the [non-microsoft] team with `read` 13 | permissions. 14 | 15 | [microsoft]: https://github.com/orgs/dotnet/teams/microsoft/repositories 16 | [non-microsoft]: https://github.com/orgs/dotnet/teams/non-microsoft/repositories -------------------------------------------------------------------------------- /doc/PR15.md: -------------------------------------------------------------------------------- 1 | # PR15 Repo must have a Code of Conduct 2 | 3 | Public repos need to include a Code of Conduct (CoC), in a file named 4 | `CODE-OF-CONDUCT.md`. 5 | 6 | The file shouldn't contain the contents of the CoC text but instead link to the 7 | latest version so that when we adopt later versions, it takes effect in all 8 | repos immediately. 9 | 10 | ## Mentions in README.md and CONTRIBUTING.md 11 | 12 | In order to keep things simple, it's best to avoid duplicating the content of 13 | `CODE-OF-CONDUCT.md`. Thus, it's probably best to remove all mentions of the 14 | Code of Conduct from all other files, such as `README.md` and `CONTRIBUTING.md`. 15 | 16 | ## .NET Foundation projects 17 | 18 | Projects that are in the `dotnet`, `aspnet`, or `mono` org are considered .NET 19 | Foundation projects. For those we recommend the following text: 20 | 21 | ```Markdown 22 | # Code of Conduct 23 | 24 | This project has adopted the code of conduct defined by the Contributor Covenant 25 | to clarify expected behavior in our community. 26 | 27 | For more information, see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct). 28 | ``` 29 | 30 | ## Microsoft projects 31 | 32 | Non-.NET Foundation open source projects should use the following text: 33 | 34 | ```Markdown 35 | # Code of Conduct 36 | 37 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct) 38 | to clarify expected behavior in our community. 39 | ``` 40 | -------------------------------------------------------------------------------- /doc/PR16.md: -------------------------------------------------------------------------------- 1 | # PR16 Repos must link correct Code of Conduct 2 | 3 | Repos should link to the correct Code of Conduct. This rule will flag repos that 4 | either don't link to the Code of Conduct at all or don't link the correct one. 5 | 6 | For more details, see [PR15](PR15.md). 7 | -------------------------------------------------------------------------------- /doc/PR17.md: -------------------------------------------------------------------------------- 1 | # PR17 Teams should have a sufficient number of maintainers 2 | 3 | In order to make sure that a team can be maintained without bottleneck, it needs 4 | a sufficient number of maintainers. We recommend that each team has at least two 5 | maintainer, not counting org owners. 6 | -------------------------------------------------------------------------------- /doc/PR18.md: -------------------------------------------------------------------------------- 1 | # PR18 Repos shouldn't use deprecated branch names 2 | 3 | We've decided to use `main` as the default branch moving forward. This will flag 4 | repos that use the old terminology. 5 | -------------------------------------------------------------------------------- /doc/PR19.md: -------------------------------------------------------------------------------- 1 | # PR19 Default branches should have branch protection 2 | 3 | The default branches should have branch protection rules, such as preventing 4 | force pushes and requiring PRs. 5 | -------------------------------------------------------------------------------- /doc/PR20.md: -------------------------------------------------------------------------------- 1 | # PR20 Release branches should have branch protection 2 | 3 | Release branches should have branch protection rules, such as preventing force 4 | pushes and requiring PRs. 5 | -------------------------------------------------------------------------------- /doc/PR21.md: -------------------------------------------------------------------------------- 1 | # PR21 Microsoft-owned repo should not use secrets 2 | 3 | Microsoft-owned repos should not leverage GitHub secrets because we assume that 4 | all secrets are maintained on Azure DevOps mirrors. 5 | -------------------------------------------------------------------------------- /doc/PR22.md: -------------------------------------------------------------------------------- 1 | # PR22 Microsoft-owned repo should disable GitHub Actions when it's not used 2 | 3 | Microsoft-owned repos should [restrict] which GitHub Actions can be used. If the 4 | repo doesn't have any workflows, it should simply disable GitHub Actions 5 | entirely. 6 | 7 | [restrict]: https://docs.github.com/en/github/administering-a-repository/managing-repository-settings/disabling-or-limiting-github-actions-for-a-repository#managing-github-actions-permissions-for-your-repository 8 | -------------------------------------------------------------------------------- /doc/PR23.md: -------------------------------------------------------------------------------- 1 | # PR23 Microsoft-owned repos should restrict GitHub Actions 2 | 3 | Microsoft-owned repos should [restrict] which GitHub Actions can be used. For 4 | repos that use workflows, the actions should be restricted to **Local Only** or 5 | [by specifying a list of patterns][patterns] that describe which actions are 6 | allowed. 7 | 8 | [restrict]: https://docs.github.com/en/github/administering-a-repository/managing-repository-settings/disabling-or-limiting-github-actions-for-a-repository#managing-github-actions-permissions-for-your-repository 9 | [patterns]: https://docs.github.com/en/github/administering-a-repository/managing-repository-settings/disabling-or-limiting-github-actions-for-a-repository#allowing-specific-actions-to-run -------------------------------------------------------------------------------- /doc/PR24.md: -------------------------------------------------------------------------------- 1 | # PR24 Microsoft-owned repos should not give read access to external contributors 2 | 3 | For read access, users should be added to the [external-ci-access] team and that 4 | team should grant read access to repo. 5 | 6 | [external-ci-access]: https://github.com/orgs/dotnet/teams/external-ci-access/members -------------------------------------------------------------------------------- /doc/PR25.md: -------------------------------------------------------------------------------- 1 | # PR25 Microsoft-owned private repos should not grant access via any non-Microsoft owned teams 2 | 3 | Repos shouldn't give any access to non-Microsoft owned teams. The team should 4 | either be removed from the repo or the team should be owned by Microsoft (which 5 | in turn will mean it can't contain any non-Microsoft users). 6 | 7 | In the rare case where giving non-Microsoft users access is intentional, a 8 | justification needs to be provided in the opened policy violation issue, marked 9 | as overridden, and closed. 10 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | # Policy rules 2 | 3 | Rule | Severity | Title 4 | ----------------|----------|------------------------------------------------ 5 | [PR01](PR01.md) | Error | Microsoft employees should be linked 6 | [PR02](PR02.md) | Error | Microsoft-owned repo should at most grant 'triage' to externals 7 | [PR03](PR03.md) | Error | Microsoft-owned team should only contain Microsoft users 8 | [PR04](PR04.md) | Error | Team should be owned by Microsoft 9 | [PR05](PR05.md) | Error | Marker team should only grant 'read' access 10 | [PR06](PR06.md) | Warning | Inactive repos should be archived 11 | [PR07](PR07.md) | Warning | Unused team should be removed 12 | [PR08](PR08.md) | Error | Too many repo admins 13 | [PR09](PR09.md) | Error | Too many team maintainers 14 | [PR10](PR10.md) | Error | Admins should be in teams 15 | [PR11](PR11.md) | Warning | Repos should have a sufficient number of admins 16 | [PR12](PR12.md) | Warning | Bots should be in the 'bots' team 17 | [PR13](PR13.md) | Warning | Collaborator access is superfluous 18 | [PR14](PR14.md) | Error | Repo ownership must be explicit 19 | [PR15](PR15.md) | Error | Repo must have a Code of Conduct 20 | [PR16](PR16.md) | Error | Repos must link correct Code of Conduct 21 | [PR17](PR17.md) | Warning | Teams should have a sufficient number of maintainers 22 | [PR18](PR18.md) | Warning | Repos shouldn't use deprecated branch names 23 | [PR19](PR19.md) | Warning | Default branches should have branch protection 24 | [PR20](PR20.md) | Warning | Release branches should have branch protection 25 | [PR21](PR21.md) | Warning | Microsoft-owned repo should not use secrets 26 | [PR22](PR22.md) | Error | Microsoft-owned repo should disable GitHub Actions when it's not used 27 | [PR23](PR23.md) | Error | Microsoft-owned repos should restrict GitHub Actions 28 | [PR24](PR24.md) | Warning | Microsoft-owned repos should not give read access to external contributors 29 | [PR25](PR25.md) | Error | Microsoft-owned private repos should not grant access via any non-Microsoft owned teams 30 | 31 | ## Process 32 | 33 | This tool runs automatically and will file policy issues in here, labeled with 34 | `area-violation` and with the particular policy (e.g. `PR01`) and assign it to 35 | the appropriate user: 36 | 37 | * For org issues, it will assign the org owners. 38 | * For repo issues, it will assign the repo admins. If there are no admins, it 39 | will assign the org owners. 40 | * For team issues, it will assign the maintainers. If there are no maintainers, 41 | it will assign the org owners. 42 | * For issues with a specific user account, it will assign the user. 43 | 44 | If an issue already exists but was closed, it will reopen the issue. If the 45 | assignees believe the policy violation is necessary, they should loop in the 46 | [@policy-cops] team. 47 | 48 | When consensus is reached that the violation is acceptable, the [@policy-cops] 49 | will label the issue `policy-override` and close it. The tool will not reopen 50 | such issues. 51 | 52 | ## Policy modification 53 | 54 | If you think new polices are needed or policies should be changed, file an 55 | issue. 56 | -------------------------------------------------------------------------------- /lib/Interop.Microsoft.Office.Interop.Excel.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet/org-policy/eb69012d539b00f3239c34fbe690d2d22d6a57ac/lib/Interop.Microsoft.Office.Interop.Excel.dll -------------------------------------------------------------------------------- /lib/Interop.Microsoft.Office.Interop.Outlook.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotnet/org-policy/eb69012d539b00f3239c34fbe690d2d22d6a57ac/lib/Interop.Microsoft.Office.Interop.Outlook.dll -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /policop.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal 3 | set PROJECT_FILE=%~dp0src\policop\policop.csproj 4 | dotnet run --project %PROJECT_FILE% -- %* 5 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Copyright (c) Microsoft 5 | Microsoft 6 | dotnet-org-policy 7 | True 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Microsoft.Csv.Excel/ExcelExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Runtime.InteropServices; 3 | 4 | using Microsoft.Office.Interop.Excel; 5 | using Microsoft.Win32; 6 | 7 | using Range = Microsoft.Office.Interop.Excel.Range; 8 | 9 | namespace Microsoft.Csv; 10 | 11 | public static class ExcelExtensions 12 | { 13 | public static bool IsExcelInstalled() 14 | { 15 | return OperatingSystem.IsWindows() && 16 | Registry.ClassesRoot.OpenSubKey("Excel.Application") is not null; 17 | } 18 | 19 | private static void AssertExcelIsInstalled() 20 | { 21 | if (!IsExcelInstalled()) 22 | throw new NotSupportedException("You do not have Excel installed. Sorry."); 23 | } 24 | 25 | private static string GetTempCsvFile() 26 | { 27 | var tempFileName = Path.GetTempFileName(); 28 | var csvFileName = Path.ChangeExtension(tempFileName, ".csv"); 29 | File.Move(tempFileName, csvFileName); 30 | return csvFileName; 31 | } 32 | 33 | public static void ViewInExcel(this CsvDocument csvDocument) 34 | { 35 | AssertExcelIsInstalled(); 36 | Debug.Assert(OperatingSystem.IsWindows()); 37 | 38 | var a = new Application(); 39 | try 40 | { 41 | a.DisplayAlerts = false; 42 | LoadCsvDocument(a, csvDocument); 43 | } 44 | finally 45 | { 46 | a.Visible = true; 47 | a.DisplayAlerts = true; 48 | Marshal.ReleaseComObject(a); 49 | } 50 | } 51 | 52 | public static void SaveToExcel(this CsvDocument csvDocument, string fileName) 53 | { 54 | AssertExcelIsInstalled(); 55 | Debug.Assert(OperatingSystem.IsWindows()); 56 | 57 | var a = new Application(); 58 | try 59 | { 60 | a.DisplayAlerts = false; 61 | LoadCsvDocument(a, csvDocument); 62 | a.ActiveWorkbook.SaveAs(fileName, XlFileFormat.xlOpenXMLWorkbook, CreateBackup: false); 63 | } 64 | finally 65 | { 66 | a.Quit(); 67 | Marshal.ReleaseComObject(a); 68 | } 69 | } 70 | 71 | private static void LoadCsvDocument(Application a, CsvDocument csvDocument) 72 | { 73 | ImportCsvDocument(a, csvDocument); 74 | FormatAsTable(a); 75 | SelectFirstCell(a); 76 | } 77 | 78 | private static void ImportCsvDocument(Application a, CsvDocument csvDocument) 79 | { 80 | var tempCsvFile = GetTempCsvFile(); 81 | try 82 | { 83 | csvDocument.Save(tempCsvFile); 84 | 85 | var targetSheet = a.Workbooks.Add().ActiveSheet; 86 | 87 | a.ScreenUpdating = false; 88 | try 89 | { 90 | var workbook = a.Workbooks.Open(tempCsvFile); 91 | try 92 | { 93 | Range range = workbook.Sheets[1].Range("A1"); 94 | range.CurrentRegion.Copy(targetSheet.Range("A1")); 95 | } 96 | finally 97 | { 98 | workbook.Close(false); 99 | } 100 | } 101 | finally 102 | { 103 | a.ScreenUpdating = true; 104 | } 105 | } 106 | finally 107 | { 108 | File.Delete(tempCsvFile); 109 | } 110 | } 111 | 112 | private static void FormatAsTable(Application a) 113 | { 114 | SelectFirstCell(a); 115 | a.Range[a.Selection, a.Selection.End(XlDirection.xlToRight)].Select(); 116 | a.Range[a.Selection, a.Selection.End(XlDirection.xlDown)].Select(); 117 | 118 | var table = a.ActiveSheet.ListObjects.Add(XlListObjectHasHeaders: XlYesNoGuess.xlYes); 119 | table.Name = "Table1"; 120 | table.TableStyle = "TableStyleLight2"; 121 | } 122 | 123 | private static void SelectFirstCell(Application a) 124 | { 125 | a.Range["A1"].Select(); 126 | } 127 | 128 | public static CsvDocument FromExcel(string fileName) 129 | { 130 | AssertExcelIsInstalled(); 131 | 132 | var csvFileName = GetTempCsvFile(); 133 | var xlsxFileName = fileName; 134 | 135 | try 136 | { 137 | ConvertToCsv(csvFileName, xlsxFileName); 138 | return CsvDocument.Load(csvFileName); 139 | } 140 | finally 141 | { 142 | File.Delete(csvFileName); 143 | } 144 | } 145 | 146 | private static void ConvertToCsv(string csvFileName, string xlsxFileName) 147 | { 148 | var a = new Application(); 149 | try 150 | { 151 | a.DisplayAlerts = false; 152 | a.Workbooks.Open(xlsxFileName); 153 | a.ActiveWorkbook.SaveAs(csvFileName, XlFileFormat.xlCSV, CreateBackup: false); 154 | a.ActiveWorkbook.Close(); 155 | } 156 | finally 157 | { 158 | // Close Excel 159 | a.Quit(); 160 | } 161 | } 162 | 163 | public static void WriteHyperlink(this CsvWriter writer, string url, string text, bool useFormula = true) 164 | { 165 | text = useFormula 166 | ? $"=HYPERLINK(\"{url}\", \"{text}\")" 167 | : text; 168 | writer.Write(text); 169 | } 170 | } -------------------------------------------------------------------------------- /src/Microsoft.Csv.Excel/Microsoft.Csv.Excel.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | true 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Microsoft.Csv/CsvDocumentReader.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.Csv; 2 | 3 | internal sealed class CsvDocumentReader : CsvReader 4 | { 5 | private readonly IList _keys; 6 | private readonly IList> _rows; 7 | private int _currentRow; 8 | 9 | public CsvDocumentReader(IList keys, IList> rows) 10 | : base(CsvSettings.Default) 11 | { 12 | _keys = keys; 13 | _rows = rows; 14 | } 15 | 16 | public override IEnumerable? Read() 17 | { 18 | if (_currentRow >= _rows.Count) 19 | return null; 20 | 21 | var row = _rows[_currentRow]; 22 | var result = new string[_keys.Count]; 23 | for (var i = 0; i < _keys.Count; i++) 24 | { 25 | var key = _keys[i]; 26 | result[i] = row[key]; 27 | } 28 | _currentRow++; 29 | return result; 30 | } 31 | } -------------------------------------------------------------------------------- /src/Microsoft.Csv/CsvDocumentWriter.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.Csv; 2 | 3 | internal sealed class CsvDocumentWriter : CsvWriter 4 | { 5 | private readonly IList _keys; 6 | private readonly IList> _rows; 7 | 8 | private int _currentKey; 9 | private IDictionary _currentRow = new Dictionary(); 10 | 11 | public CsvDocumentWriter(IList keys, IList> rows) 12 | : base(CsvSettings.Default) 13 | { 14 | _keys = keys; 15 | _rows = rows; 16 | } 17 | 18 | public override void Write(string value) 19 | { 20 | var key = _keys[_currentKey]; 21 | _currentRow[key] = value; 22 | _currentKey++; 23 | } 24 | 25 | public override void WriteLine() 26 | { 27 | _rows.Add(_currentRow); 28 | _currentKey = 0; 29 | _currentRow = new Dictionary(); 30 | } 31 | } -------------------------------------------------------------------------------- /src/Microsoft.Csv/CsvFile.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.Csv; 2 | 3 | public static class CsvFile 4 | { 5 | public static CsvTextReader Read(string fileName) 6 | { 7 | ArgumentNullException.ThrowIfNull(fileName); 8 | 9 | return Read(fileName, CsvSettings.Default); 10 | } 11 | 12 | public static CsvTextReader Read(string fileName, CsvSettings settings) 13 | { 14 | ArgumentNullException.ThrowIfNull(fileName); 15 | 16 | var fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read); 17 | var streamReader = new StreamReader(fileStream, settings.Encoding); 18 | return new CsvTextReader(streamReader, settings); 19 | } 20 | 21 | public static CsvTextWriter Create(string fileName) 22 | { 23 | ArgumentNullException.ThrowIfNull(fileName); 24 | 25 | return Create(fileName, CsvSettings.Default); 26 | } 27 | 28 | public static CsvTextWriter Create(string fileName, CsvSettings settings) 29 | { 30 | ArgumentNullException.ThrowIfNull(fileName); 31 | 32 | var fileStream = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None); 33 | var streamWriter = new StreamWriter(fileStream, settings.Encoding); 34 | return new CsvTextWriter(streamWriter, settings); 35 | } 36 | 37 | public static CsvTextWriter Append(string fileName) 38 | { 39 | ArgumentNullException.ThrowIfNull(fileName); 40 | 41 | return Append(fileName, CsvSettings.Default); 42 | } 43 | 44 | public static CsvTextWriter Append(string fileName, CsvSettings settings) 45 | { 46 | ArgumentNullException.ThrowIfNull(fileName); 47 | 48 | var fileStream = new FileStream(fileName, FileMode.Append, FileAccess.Write, FileShare.None); 49 | var streamWriter = new StreamWriter(fileStream, settings.Encoding); 50 | return new CsvTextWriter(streamWriter, settings); 51 | } 52 | 53 | public static IEnumerable> ReadLines(string fileName) 54 | { 55 | ArgumentNullException.ThrowIfNull(fileName); 56 | 57 | return ReadLines(fileName, CsvSettings.Default); 58 | } 59 | 60 | public static IEnumerable> ReadLines(string fileName, CsvSettings settings) 61 | { 62 | ArgumentNullException.ThrowIfNull(fileName); 63 | 64 | using (var csvReader = Read(fileName, settings)) 65 | { 66 | var line = csvReader.Read(); 67 | while (line is not null) 68 | { 69 | yield return line; 70 | line = csvReader.Read(); 71 | } 72 | } 73 | } 74 | 75 | public static void WriteLines(string fileName, IEnumerable> lines) 76 | { 77 | ArgumentNullException.ThrowIfNull(fileName); 78 | ArgumentNullException.ThrowIfNull(lines); 79 | 80 | WriteLines(fileName, lines, CsvSettings.Default); 81 | } 82 | 83 | public static void WriteLines(string fileName, IEnumerable header, IEnumerable> lines) 84 | { 85 | ArgumentNullException.ThrowIfNull(fileName); 86 | ArgumentNullException.ThrowIfNull(header); 87 | ArgumentNullException.ThrowIfNull(lines); 88 | 89 | WriteLines(fileName, header, lines, CsvSettings.Default); 90 | } 91 | 92 | public static void WriteLines(string fileName, IEnumerable header, IEnumerable> lines, CsvSettings settings) 93 | { 94 | ArgumentNullException.ThrowIfNull(fileName); 95 | ArgumentNullException.ThrowIfNull(header); 96 | 97 | var headerLine = new[] { header }; 98 | var allLines = headerLine.Concat(lines); 99 | WriteLines(fileName, allLines, settings); 100 | } 101 | 102 | public static void WriteLines(string fileName, IEnumerable> lines, CsvSettings settings) 103 | { 104 | ArgumentNullException.ThrowIfNull(fileName); 105 | ArgumentNullException.ThrowIfNull(lines); 106 | 107 | using (var csvWriter = Create(fileName, settings)) 108 | { 109 | foreach (var line in lines) 110 | csvWriter.WriteLine(line); 111 | } 112 | } 113 | 114 | public static void AppendLines(string fileName, IEnumerable> lines) 115 | { 116 | ArgumentNullException.ThrowIfNull(fileName); 117 | ArgumentNullException.ThrowIfNull(lines); 118 | 119 | AppendLines(fileName, lines, CsvSettings.Default); 120 | } 121 | 122 | public static void AppendLines(string fileName, IEnumerable header, IEnumerable> lines) 123 | { 124 | ArgumentNullException.ThrowIfNull(fileName); 125 | ArgumentNullException.ThrowIfNull(header); 126 | ArgumentNullException.ThrowIfNull(lines); 127 | 128 | AppendLines(fileName, header, lines, CsvSettings.Default); 129 | } 130 | 131 | public static void AppendLines(string fileName, IEnumerable header, IEnumerable> lines, CsvSettings settings) 132 | { 133 | ArgumentNullException.ThrowIfNull(fileName); 134 | ArgumentNullException.ThrowIfNull(header); 135 | 136 | var headerLine = new[] { header }; 137 | var allLines = headerLine.Concat(lines); 138 | AppendLines(fileName, allLines, settings); 139 | } 140 | 141 | public static void AppendLines(string fileName, IEnumerable> lines, CsvSettings settings) 142 | { 143 | ArgumentNullException.ThrowIfNull(fileName); 144 | ArgumentNullException.ThrowIfNull(lines); 145 | 146 | using (var csvWriter = Append(fileName, settings)) 147 | { 148 | foreach (var line in lines) 149 | csvWriter.WriteLine(line); 150 | } 151 | } 152 | } -------------------------------------------------------------------------------- /src/Microsoft.Csv/CsvLineReader.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Text; 3 | 4 | namespace Microsoft.Csv; 5 | 6 | internal sealed class CsvLineReader : IDisposable, IEnumerable> 7 | { 8 | private readonly TextReader _textReader; 9 | private readonly CsvSettings _settings; 10 | private char _next; 11 | private readonly List _fields = new List(); 12 | private readonly StringBuilder _sb = new StringBuilder(); 13 | 14 | private const char Eof = '\0'; 15 | private const char CarriageReturn = '\r'; 16 | private const char LineFeed = '\n'; 17 | 18 | public CsvLineReader(TextReader textReader, CsvSettings settings) 19 | { 20 | _textReader = textReader; 21 | _settings = settings; 22 | _next = ToChar(_textReader.Peek()); 23 | } 24 | 25 | public void Dispose() 26 | { 27 | _textReader.Dispose(); 28 | } 29 | 30 | private char Read() 31 | { 32 | var current = ToChar(_textReader.Read()); 33 | _next = ToChar(_textReader.Peek()); 34 | return current; 35 | } 36 | 37 | private char Peek() 38 | { 39 | return _next; 40 | } 41 | 42 | private static char ToChar(int c) 43 | { 44 | return c < 0 45 | ? Eof 46 | : (char)c; 47 | } 48 | 49 | public IEnumerator> GetEnumerator() 50 | { 51 | var line = ReadLine(); 52 | while (line is not null) 53 | { 54 | yield return line; 55 | line = ReadLine(); 56 | } 57 | } 58 | 59 | IEnumerator IEnumerable.GetEnumerator() 60 | { 61 | return GetEnumerator(); 62 | } 63 | 64 | private IEnumerable? ReadLine() 65 | { 66 | if (Peek() == Eof) 67 | return null; 68 | 69 | _fields.Clear(); 70 | var field = ReadField(); 71 | while (field is not null) 72 | { 73 | _fields.Add(field); 74 | field = ReadField(); 75 | } 76 | 77 | return _fields; 78 | } 79 | 80 | private string? ReadField() 81 | { 82 | if (Peek() == CarriageReturn) 83 | { 84 | Read(); 85 | 86 | if (Peek() == LineFeed) 87 | Read(); 88 | 89 | return null; 90 | } 91 | 92 | if (Peek() == LineFeed) 93 | { 94 | Read(); 95 | return null; 96 | } 97 | 98 | ReadWhitespace(); 99 | 100 | var firstChar = Peek(); 101 | if (firstChar == Eof) 102 | return null; 103 | 104 | return firstChar == _settings.TextQualifier 105 | ? ReadQualifiedField() 106 | : ReadUnqualifiedField(); 107 | } 108 | 109 | private void ReadWhitespace() 110 | { 111 | var c = Peek(); 112 | while (char.IsWhiteSpace(c) && c != _settings.Delimiter && c != CarriageReturn && c != LineFeed && c != Eof) 113 | { 114 | Read(); 115 | c = Peek(); 116 | } 117 | } 118 | 119 | private string ReadQualifiedField() 120 | { 121 | // Skip first quote 122 | Read(); 123 | 124 | _sb.Clear(); 125 | var c = Read(); 126 | while (c != Eof) 127 | { 128 | if (c == _settings.TextQualifier) 129 | { 130 | if (Peek() == _settings.TextQualifier) 131 | { 132 | // Escaped quote 133 | // Skip one of the two qotes. 134 | Read(); 135 | } 136 | else 137 | { 138 | // End of field 139 | break; 140 | } 141 | } 142 | _sb.Append(c); 143 | c = Read(); 144 | } 145 | 146 | var result = _sb.ToString(); 147 | 148 | // Skip everything up to and including the separator. 149 | ReadUnqualifiedField(); 150 | 151 | return result; 152 | } 153 | 154 | private string ReadUnqualifiedField() 155 | { 156 | var c = Peek(); 157 | _sb.Clear(); 158 | while (c != _settings.Delimiter && c != CarriageReturn && c != LineFeed && c != Eof) 159 | { 160 | _sb.Append(c); 161 | Read(); 162 | c = Peek(); 163 | } 164 | 165 | if (c == _settings.Delimiter) 166 | Read(); 167 | 168 | return _sb.ToString(); 169 | } 170 | } -------------------------------------------------------------------------------- /src/Microsoft.Csv/CsvReader.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.Csv; 2 | 3 | public abstract class CsvReader : IDisposable 4 | { 5 | protected CsvReader(CsvSettings settings) 6 | { 7 | Settings = settings; 8 | } 9 | 10 | public void Dispose() 11 | { 12 | Dispose(true); 13 | GC.SuppressFinalize(this); 14 | } 15 | 16 | protected virtual void Dispose(bool disposing) 17 | { 18 | } 19 | 20 | public abstract IEnumerable? Read(); 21 | 22 | public CsvSettings Settings { get; set; } 23 | } -------------------------------------------------------------------------------- /src/Microsoft.Csv/CsvSettings.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace Microsoft.Csv; 4 | 5 | public readonly struct CsvSettings 6 | { 7 | public static CsvSettings Default { get; } = new CsvSettings( 8 | encoding: Encoding.UTF8, 9 | delimiter: ',', 10 | textQualifier: '"' 11 | ); 12 | 13 | public CsvSettings(Encoding encoding, char delimiter, char textQualifier) 14 | : this() 15 | { 16 | ArgumentNullException.ThrowIfNull(encoding); 17 | 18 | Encoding = encoding; 19 | Delimiter = delimiter; 20 | TextQualifier = textQualifier; 21 | } 22 | 23 | public Encoding Encoding { get; } 24 | public char Delimiter { get; } 25 | public char TextQualifier { get; } 26 | 27 | public bool IsValid => Encoding is not null; 28 | } -------------------------------------------------------------------------------- /src/Microsoft.Csv/CsvTextReader.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.Csv; 2 | 3 | public class CsvTextReader : CsvReader 4 | { 5 | private readonly CsvLineReader _reader; 6 | private readonly IEnumerator> _enumerator; 7 | 8 | public CsvTextReader(TextReader textReader, CsvSettings settings) 9 | : base(settings) 10 | { 11 | ArgumentNullException.ThrowIfNull(textReader); 12 | 13 | _reader = new CsvLineReader(textReader, Settings); 14 | _enumerator = _reader.GetEnumerator(); 15 | } 16 | 17 | protected override void Dispose(bool disposing) 18 | { 19 | if (disposing) 20 | _reader.Dispose(); 21 | } 22 | 23 | public override IEnumerable? Read() 24 | { 25 | if (!_enumerator.MoveNext()) 26 | return null; 27 | 28 | var line = _enumerator.Current; 29 | return line; 30 | } 31 | } -------------------------------------------------------------------------------- /src/Microsoft.Csv/CsvTextWriter.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace Microsoft.Csv; 4 | 5 | public class CsvTextWriter : CsvWriter 6 | { 7 | private readonly TextWriter _textWriter; 8 | private bool _valuesSeen; 9 | private readonly char[] _textDelimiters; 10 | 11 | public CsvTextWriter(TextWriter textWriter) 12 | : this(textWriter, CsvSettings.Default) 13 | { 14 | } 15 | 16 | public CsvTextWriter(TextWriter textWriter, CsvSettings settings) 17 | : base(settings) 18 | { 19 | ArgumentNullException.ThrowIfNull(textWriter); 20 | 21 | _textWriter = textWriter; 22 | _textDelimiters = new char[] { settings.Delimiter, settings.TextQualifier, '\r', '\n' }; 23 | } 24 | 25 | public override CsvSettings Settings 26 | { 27 | get => base.Settings; 28 | set 29 | { 30 | base.Settings = value; 31 | _textDelimiters[0] = value.Delimiter; 32 | _textDelimiters[1] = value.TextQualifier; 33 | } 34 | } 35 | 36 | protected override void Dispose(bool disposing) 37 | { 38 | if (disposing) 39 | _textWriter.Dispose(); 40 | } 41 | 42 | public override void Write(string value) 43 | { 44 | if (_valuesSeen) 45 | _textWriter.Write(Settings.Delimiter); 46 | 47 | _valuesSeen = true; 48 | var escapedText = EscapeValue(value); 49 | _textWriter.Write(escapedText); 50 | } 51 | 52 | public override void WriteLine() 53 | { 54 | if (_valuesSeen) 55 | { 56 | _valuesSeen = false; 57 | _textWriter.WriteLine(); 58 | } 59 | } 60 | 61 | protected string EscapeValue(string value) 62 | { 63 | if (value is null) 64 | return string.Empty; 65 | 66 | var textQualifier = Settings.TextQualifier; 67 | var needsEscaping = value.IndexOfAny(_textDelimiters) >= 0; 68 | if (!needsEscaping) 69 | return value; 70 | 71 | var sb = new StringBuilder(value.Length + 2); 72 | sb.Append(textQualifier); 73 | foreach (var c in value) 74 | { 75 | if (c == textQualifier) 76 | sb.Append(textQualifier); 77 | 78 | sb.Append(c); 79 | } 80 | sb.Append(textQualifier); 81 | return sb.ToString(); 82 | } 83 | } -------------------------------------------------------------------------------- /src/Microsoft.Csv/CsvWriter.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.Csv; 2 | 3 | public abstract class CsvWriter : IDisposable 4 | { 5 | private CsvSettings _settings; 6 | 7 | protected CsvWriter(CsvSettings settings) 8 | { 9 | _settings = settings; 10 | } 11 | 12 | public void Dispose() 13 | { 14 | Dispose(true); 15 | GC.SuppressFinalize(this); 16 | } 17 | 18 | protected virtual void Dispose(bool disposing) 19 | { 20 | } 21 | 22 | public abstract void Write(string value); 23 | 24 | public virtual void Write(IEnumerable values) 25 | { 26 | foreach (var value in values) 27 | Write(value); 28 | } 29 | 30 | public abstract void WriteLine(); 31 | 32 | public virtual void WriteLine(IEnumerable values) 33 | { 34 | foreach (var value in values) 35 | Write(value); 36 | 37 | WriteLine(); 38 | } 39 | 40 | public virtual CsvSettings Settings 41 | { 42 | get => _settings; 43 | set => _settings = value; 44 | } 45 | } -------------------------------------------------------------------------------- /src/Microsoft.Csv/Microsoft.Csv.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.GitHubCaching/CachedAccessReason.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | public readonly struct CachedAccessReason 4 | { 5 | public static CachedAccessReason FromOwner => new CachedAccessReason(isOwner: true, isCollaborator: false, null); 6 | public static CachedAccessReason FromCollaborator => new CachedAccessReason(isOwner: false, isCollaborator: true, null); 7 | public static CachedAccessReason FromTeam(CachedTeam team) 8 | { 9 | ArgumentNullException.ThrowIfNull(team); 10 | 11 | return new CachedAccessReason(isOwner: false, isCollaborator: false, team); 12 | } 13 | 14 | private CachedAccessReason(bool isOwner, bool isCollaborator, CachedTeam? team) 15 | { 16 | IsOwner = isOwner; 17 | IsCollaborator = isCollaborator; 18 | Team = team; 19 | } 20 | 21 | public bool IsOwner { get; } 22 | public bool IsCollaborator { get; } 23 | public CachedTeam? Team { get; } 24 | 25 | public override string ToString() 26 | { 27 | if (IsOwner) 28 | return "(Owner)"; 29 | 30 | if (IsCollaborator) 31 | return "(Collaborator)"; 32 | 33 | return Team!.GetFullName(); 34 | } 35 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.GitHubCaching/CachedActionPermissions.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace Microsoft.DotnetOrg.GitHubCaching; 3 | 4 | public sealed class CachedActionPermissions 5 | { 6 | public bool Enabled { get; set; } 7 | public CachedRepoAllowedActions AllowedActions { get; set; } 8 | public bool GitHubOwnedAllowed { get; set; } 9 | public bool VerifiedAllowed { get; set; } 10 | public string[] PatternsAllowed { get; set; } = Array.Empty(); 11 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.GitHubCaching/CachedBranch.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Microsoft.DotnetOrg.GitHubCaching; 4 | #pragma warning disable CS8618 // This is a serialized type. 5 | public sealed class CachedBranch 6 | { 7 | [JsonIgnore] 8 | public string Ref => $"{Prefix}/{Name}"; 9 | 10 | public string Prefix { get; set; } 11 | public string Name { get; set; } 12 | public string Hash { get; set; } 13 | 14 | [JsonIgnore] 15 | public CachedOrg Org => Repo?.Org!; 16 | 17 | [JsonIgnore] 18 | public CachedRepo Repo { get; set; } 19 | 20 | [JsonIgnore] 21 | public string Url => CachedOrg.GetBranchUrl(Org.Name, Repo.Name, Name); 22 | 23 | [JsonIgnore] 24 | public IEnumerable Rules => Repo.BranchProtectionRules.Where(r => r.MatchingRefs.Contains(Ref)); 25 | 26 | public override string ToString() 27 | { 28 | return Name; 29 | } 30 | } 31 | #pragma warning restore CS8618 -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.GitHubCaching/CachedBranchProtectionRule.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Microsoft.DotnetOrg.GitHubCaching; 4 | #pragma warning disable CS8618 // This is a serialized type. 5 | public sealed class CachedBranchProtectionRule 6 | { 7 | public bool DismissesStaleReviews { get; set; } 8 | public bool IsAdminEnforced { get; set; } 9 | public string Pattern { get; set; } 10 | public int? RequiredApprovingReviewCount { get; set; } 11 | public IReadOnlyList RequiredStatusCheckContexts { get; set; } 12 | public bool RequiresApprovingReviews { get; set; } 13 | public bool RequiresCodeOwnerReviews { get; set; } 14 | public bool RequiresCommitSignatures { get; set; } 15 | public bool RequiresStatusChecks { get; set; } 16 | public bool RequiresStrictStatusChecks { get; set; } 17 | public bool RestrictsPushes { get; set; } 18 | public bool RestrictsReviewDismissals { get; set; } 19 | public IReadOnlyList MatchingRefs { get; set; } 20 | 21 | [JsonIgnore] 22 | public CachedRepo Repo { get; set; } 23 | } 24 | #pragma warning restore -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.GitHubCaching/CachedFile.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.DotnetOrg.GitHubCaching; 2 | #pragma warning disable CS8618 // This is a serialized type. 3 | public sealed class CachedFile 4 | { 5 | public string Name { get; set; } 6 | public string Contents { get; set; } 7 | public string Url { get; set; } 8 | } 9 | #pragma warning restore CS8618 -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.GitHubCaching/CachedOrgSecret.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Microsoft.DotnetOrg.GitHubCaching; 4 | #pragma warning disable CS8618 // Serialized type 5 | public sealed class CachedOrgSecret : CachedSecret 6 | { 7 | public string Visibility { get; set; } 8 | public IReadOnlyList RepositoryNames { get; set; } 9 | 10 | [JsonIgnore] 11 | public CachedOrg Org { get; set; } 12 | 13 | [JsonIgnore] 14 | public IReadOnlyList Repositories { get; set; } 15 | 16 | [JsonIgnore] 17 | public override string Url => $"https://github.com/organizations/{Org.Name}/settings/secrets/actions"; 18 | } 19 | #pragma warning restore CS8618 -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.GitHubCaching/CachedPermission.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | public enum CachedPermission 4 | { 5 | Read, 6 | Triage, 7 | Write, 8 | Maintain, 9 | Admin 10 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.GitHubCaching/CachedRepo.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Microsoft.DotnetOrg.GitHubCaching; 4 | #pragma warning disable CS8618 // This is a serialized type. 5 | public sealed class CachedRepo 6 | { 7 | public long Id { get; set; } 8 | public string Name { get; set; } 9 | public bool IsPrivate { get; set; } 10 | public bool IsArchived { get; set; } 11 | public bool IsTemplate { get; set; } 12 | public bool IsFork { get; set; } 13 | public bool IsMirror { get; set; } 14 | public DateTimeOffset LastPush { get; set; } 15 | public string Description { get; set; } 16 | public string DefaultBranchName { get; set; } 17 | public IReadOnlyList Branches { get; set; } 18 | public IReadOnlyList BranchProtectionRules { get; set; } 19 | public IReadOnlyList Environments { get; set; } 20 | public IReadOnlyList Secrets { get; set; } 21 | public IReadOnlyList Properties { get; set; } 22 | public CachedFile? ReadMe { get; set; } 23 | public CachedFile? Contributing { get; set; } 24 | public CachedFile? CodeOfConduct { get; set; } 25 | public CachedFile? License { get; set; } 26 | public CachedActionPermissions ActionPermissions { get; set; } 27 | public IReadOnlyList Workflows { get; set; } 28 | 29 | [JsonIgnore] 30 | public CachedOrg Org { get; set; } 31 | 32 | [JsonIgnore] 33 | public string Url => CachedOrg.GetRepoUrl(Org.Name, Name); 34 | 35 | [JsonIgnore] 36 | public string FullName => $"{Org.Name}/{Name}"; 37 | 38 | [JsonIgnore] 39 | // Note: Repos that have never been pushed don't have a branch yet. 40 | public CachedBranch? DefaultBranch => Branches.SingleOrDefault(b => b.Name == DefaultBranchName); 41 | 42 | [JsonIgnore] 43 | public List Teams { get; } = new List(); 44 | 45 | [JsonIgnore] 46 | public List Users { get; } = new List(); 47 | 48 | [JsonIgnore] 49 | public List EffectiveUsers { get; } = new List(); 50 | 51 | [JsonIgnore] 52 | public List OrgSecrets { get; set; } = new List(); 53 | } 54 | #pragma warning restore CS8618 -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.GitHubCaching/CachedRepoAllowedActions.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | public enum CachedRepoAllowedActions 4 | { 5 | Disabled, 6 | All, 7 | LocalOnly, 8 | Selected 9 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.GitHubCaching/CachedRepoEnvironment.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Microsoft.DotnetOrg.GitHubCaching; 4 | #pragma warning disable CS8618 // This is a serialized type. 5 | public sealed class CachedRepoEnvironment 6 | { 7 | public long Id { get; set; } 8 | public string NodeId { get; set; } 9 | public string Name { get; set; } 10 | public string Url { get; set; } 11 | public DateTimeOffset CreatedAt { get; set; } 12 | public DateTimeOffset UpdatedAt { get; set; } 13 | public IReadOnlyList Secrets { get; set; } 14 | 15 | [JsonIgnore] 16 | public CachedRepo Repo { get; set; } 17 | } 18 | #pragma warning restore CS8618 -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.GitHubCaching/CachedRepoProperty.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | public sealed class CachedRepoProperty 4 | { 5 | public CachedRepoProperty(string name, string value) 6 | { 7 | Name = name; 8 | Value = value; 9 | } 10 | 11 | public string Name { get; } 12 | 13 | public string Value { get; } 14 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.GitHubCaching/CachedRepoSecret.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Microsoft.DotnetOrg.GitHubCaching; 4 | #pragma warning disable CS8618 // Serialized type 5 | public class CachedRepoSecret : CachedSecret 6 | { 7 | [JsonIgnore] 8 | public CachedRepo Repo { get; set; } 9 | 10 | [JsonIgnore] 11 | public CachedRepoEnvironment? Environment { get; set; } 12 | 13 | [JsonIgnore] 14 | public override string Url 15 | { 16 | get 17 | { 18 | if (Environment is not null) 19 | return $"https://github.com/{Repo.Org.Name}/{Repo.Name}/settings/environments/{Environment.Id}/edit"; 20 | else 21 | return $"https://github.com/{Repo.Org.Name}/{Repo.Name}/settings/secrets/actions"; 22 | } 23 | } 24 | } 25 | #pragma warning restore CS8618 -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.GitHubCaching/CachedSecret.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Microsoft.DotnetOrg.GitHubCaching; 4 | #pragma warning disable CS8618 // Serialized type 5 | public abstract class CachedSecret 6 | { 7 | public string Name { get; set; } 8 | public DateTimeOffset CreatedAt { get; set; } 9 | public DateTimeOffset UpdatedAt { get; set; } 10 | 11 | [JsonIgnore] 12 | public abstract string Url { get; } 13 | } 14 | #pragma warning restore CS8618 -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.GitHubCaching/CachedTeam.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Microsoft.DotnetOrg.GitHubCaching; 4 | #pragma warning disable CS8618 // This is a serialized type. 5 | public sealed class CachedTeam 6 | { 7 | public string Id { get; set; } 8 | public string Slug { get; set; } 9 | public string Name { get; set; } 10 | public string? ParentId { get; set; } 11 | public string Description { get; set; } 12 | public bool IsSecret { get; set; } 13 | public SortedSet MaintainerLogins { get; set; } = new SortedSet(); 14 | public SortedSet MemberLogins { get; set; } = new SortedSet(); 15 | public List Repos { get; set; } = new List(); 16 | 17 | [JsonIgnore] 18 | public CachedOrg Org { get; set; } 19 | 20 | [JsonIgnore] 21 | public string Url => CachedOrg.GetTeamUrl(Org.Name, Slug); 22 | 23 | [JsonIgnore] 24 | public CachedTeam Parent { get; set; } 25 | 26 | [JsonIgnore] 27 | public List Children { get; } = new List(); 28 | 29 | [JsonIgnore] 30 | public List Maintainers { get; } = new List(); 31 | 32 | [JsonIgnore] 33 | public List Members { get; } = new List(); 34 | 35 | [JsonIgnore] 36 | public List EffectiveMembers { get; } = new List(); 37 | 38 | public string GetFullSlug() 39 | { 40 | var teamSlugs = AncestorsAndSelf().Reverse().Select(t => t.Slug); 41 | return string.Join("/", teamSlugs); 42 | } 43 | 44 | public string GetFullName() 45 | { 46 | var teamNames = AncestorsAndSelf().Reverse().Select(t => t.Name); 47 | return string.Join("/", teamNames); 48 | } 49 | 50 | public IEnumerable AncestorsAndSelf() 51 | { 52 | var current = this; 53 | while (current is not null) 54 | { 55 | yield return current; 56 | current = current.Parent; 57 | } 58 | } 59 | 60 | public IEnumerable DescendentsAndSelf() 61 | { 62 | var stack = new Stack(); 63 | stack.Push(this); 64 | 65 | while (stack.Count > 0) 66 | { 67 | var current = stack.Pop(); 68 | yield return current; 69 | 70 | foreach (var next in current.Children) 71 | stack.Push(next); 72 | } 73 | } 74 | } 75 | #pragma warning restore CS8618 -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.GitHubCaching/CachedTeamAccess.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Microsoft.DotnetOrg.GitHubCaching; 4 | #pragma warning disable CS8618 // This is a serialized type. 5 | public sealed class CachedTeamAccess 6 | { 7 | public string RepoName { get; set; } 8 | public CachedPermission Permission { get; set; } 9 | 10 | [JsonIgnore] 11 | public CachedOrg Org => Repo.Org; 12 | 13 | [JsonIgnore] 14 | public string TeamSlug { get; set; } 15 | 16 | [JsonIgnore] 17 | public CachedRepo Repo { get; internal set; } 18 | 19 | [JsonIgnore] 20 | public CachedTeam Team { get; set; } 21 | } 22 | #pragma warning restore CS8618 -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.GitHubCaching/CachedTeamMember.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.DotnetOrg.GitHubCaching; 2 | #pragma warning disable CS8618 // This is a serialized type. 3 | internal sealed class CachedTeamMember 4 | { 5 | public string TeamSlug { get; set; } 6 | public string UserLogin { get; set; } 7 | public bool IsMaintainer { get; set; } 8 | } 9 | #pragma warning restore CS8618 -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.GitHubCaching/CachedUser.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | using Microsoft.DotnetOrg.Ospo; 4 | 5 | namespace Microsoft.DotnetOrg.GitHubCaching; 6 | #pragma warning disable CS8618 // This is a serialized type. 7 | public sealed class CachedUser 8 | { 9 | public string Login { get; set; } 10 | public string Name { get; set; } 11 | public string Company { get; set; } 12 | public string Email { get; set; } 13 | public bool IsOwner { get; set; } 14 | public bool IsMember { get; set; } 15 | public MicrosoftInfo MicrosoftInfo { get; set; } 16 | 17 | [JsonIgnore] 18 | public CachedOrg Org { get; set; } 19 | 20 | [JsonIgnore] 21 | public string Url => CachedOrg.GetUserUrl(Login); 22 | 23 | [JsonIgnore] 24 | public bool IsExternal => !IsMember; 25 | 26 | [JsonIgnore] 27 | public List Teams { get; } = new List(); 28 | 29 | [JsonIgnore] 30 | public List Repos { get; } = new List(); 31 | } 32 | #pragma warning restore CS8618 -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.GitHubCaching/CachedUserAccess.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Microsoft.DotnetOrg.GitHubCaching; 4 | #pragma warning disable CS8618 // This is a serialized type. 5 | public sealed class CachedUserAccess 6 | { 7 | public string RepoName { get; set; } 8 | public string UserLogin { get; set; } 9 | public CachedPermission Permission { get; set; } 10 | 11 | [JsonIgnore] 12 | public CachedOrg Org => Repo.Org; 13 | 14 | [JsonIgnore] 15 | public CachedRepo Repo { get; set; } 16 | 17 | [JsonIgnore] 18 | public CachedUser User { get; set; } 19 | 20 | public CachedAccessReason Describe() 21 | { 22 | foreach (var teamAccess in Repo.Teams) 23 | { 24 | if (teamAccess.Permission == Permission) 25 | { 26 | foreach (var team in teamAccess.Team.DescendentsAndSelf()) 27 | { 28 | if (team.Members.Contains(User)) 29 | return CachedAccessReason.FromTeam(team); 30 | } 31 | } 32 | } 33 | 34 | return User.IsOwner 35 | ? CachedAccessReason.FromOwner 36 | : CachedAccessReason.FromCollaborator; 37 | } 38 | 39 | public CachedWhatIfPermission WhatIfDowngraded(CachedTeam team, CachedPermission? newPermission) 40 | { 41 | return WhatIf(ta => 42 | { 43 | if (ta.Team == team) 44 | { 45 | if (newPermission is null) 46 | return null; 47 | 48 | // Only downgrade, never upgrade 49 | if (ta.Permission >= newPermission.Value) 50 | return newPermission.Value; 51 | } 52 | 53 | return ta.Permission; 54 | }); 55 | } 56 | 57 | public CachedWhatIfPermission WhatIf(Func permissionChanger) 58 | { 59 | if (User.IsOwner) 60 | return new CachedWhatIfPermission(this, CachedPermission.Admin); 61 | 62 | // Let's start by computing what we're getting from the repo directly. 63 | // 64 | // NOTE: This currently won't work because the GitHub API has no way to tell us 65 | // direct repo permissions. For detais, see: 66 | // 67 | // https://github.com/octokit/octokit.net/issues/2036) 68 | // 69 | // Rather, it only gives us effective permissions. This means that if a user 70 | // has 'admin' permissions through a team, but 'write' permissions by directly 71 | // being added to a repo, running what-if for this repo/team will (incorrectly) 72 | // conclude that the user was downgraded to 'read' or lost access (if the repo 73 | // is private). 74 | // 75 | // However, the current code will work for cases where the permissions granted 76 | // through the team is less than what was given via the repo directly. 77 | 78 | var maximumLevel = Repo.Users.Where(ua => ua.User == User && ua.Describe().IsCollaborator) 79 | .Select(ua => (int)ua.Permission) 80 | .DefaultIfEmpty(-1) 81 | .Max(); 82 | 83 | foreach (var teamAccess in Repo.Teams) 84 | { 85 | var teamAccessLevel = (int)teamAccess.Permission; 86 | 87 | foreach (var nestedTeam in teamAccess.Team.DescendentsAndSelf()) 88 | { 89 | if (!nestedTeam.Members.Contains(User)) 90 | continue; 91 | 92 | var newPermission = permissionChanger(teamAccess); 93 | var newPermissionLevel = newPermission is null ? -1 : (int)newPermission.Value; 94 | maximumLevel = Math.Max(maximumLevel, newPermissionLevel); 95 | break; 96 | } 97 | } 98 | 99 | if (maximumLevel == -1) 100 | return new CachedWhatIfPermission(this, Repo.IsPrivate ? null : (CachedPermission?)CachedPermission.Read); 101 | 102 | var maximumPermission = (CachedPermission)maximumLevel; 103 | return new CachedWhatIfPermission(this, maximumPermission); 104 | } 105 | } 106 | #pragma warning restore CS8618 -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.GitHubCaching/CachedWhatIfPermission.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | public readonly struct CachedWhatIfPermission 4 | { 5 | public CachedWhatIfPermission(CachedUserAccess userAccess, CachedPermission? newPermissions) 6 | { 7 | UserAccess = userAccess; 8 | NewPermissions = newPermissions; 9 | } 10 | 11 | public CachedUserAccess UserAccess { get; } 12 | public CachedPermission? NewPermissions { get; } 13 | 14 | public bool IsUnchanged => NewPermissions is not null && UserAccess.Permission == NewPermissions.Value; 15 | 16 | public override string ToString() 17 | { 18 | if (UserAccess.Permission == NewPermissions) 19 | return "(unchanged)"; 20 | 21 | var oldPermission = UserAccess.Permission.ToString().ToLower(); 22 | var newPermission = NewPermissions?.ToString().ToLower() ?? "(no access)"; 23 | return $"{oldPermission} -> {newPermission}"; 24 | } 25 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.GitHubCaching/GitHubArtifact.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Microsoft.DotnetOrg.GitHubCaching; 4 | 5 | public sealed class GitHubArtifact 6 | { 7 | public GitHubArtifact(long id, 8 | string nodeId, 9 | string name, 10 | long sizeInBytes, 11 | string url, 12 | string archiveDownloadUrl, 13 | bool expired, 14 | DateTimeOffset createdAt, 15 | DateTimeOffset updatedAt, 16 | DateTimeOffset expiresAt) 17 | { 18 | Id = id; 19 | NodeId = nodeId; 20 | Name = name; 21 | SizeInBytes = sizeInBytes; 22 | Url = url; 23 | ArchiveDownloadUrl = archiveDownloadUrl; 24 | Expired = expired; 25 | CreatedAt = createdAt; 26 | UpdatedAt = updatedAt; 27 | ExpiresAt = expiresAt; 28 | } 29 | 30 | [JsonPropertyName("id")] 31 | public long Id { get; } 32 | 33 | [JsonPropertyName("node_id")] 34 | public string NodeId { get; } 35 | 36 | [JsonPropertyName("name")] 37 | public string Name { get; } 38 | 39 | [JsonPropertyName("size_in_bytes")] 40 | public long SizeInBytes { get; } 41 | 42 | [JsonPropertyName("url")] 43 | public string Url { get; } 44 | 45 | [JsonPropertyName("archive_download_url")] 46 | public string ArchiveDownloadUrl { get; } 47 | 48 | [JsonPropertyName("expired")] 49 | public bool Expired { get; } 50 | 51 | [JsonPropertyName("created_at")] 52 | public DateTimeOffset CreatedAt { get; } 53 | 54 | [JsonPropertyName("updated_at")] 55 | public DateTimeOffset UpdatedAt { get; } 56 | 57 | [JsonPropertyName("expires_at")] 58 | public DateTimeOffset ExpiresAt { get; } 59 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.GitHubCaching/GitHubCommunityProfile.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.DotnetOrg.GitHubCaching; 2 | #pragma warning disable CS8618 // This is a serialized type. 3 | public sealed class GitHubCommunityProfile 4 | { 5 | public int HealthPercentage { get; set; } 6 | public string Description { get; set; } 7 | public string Documentation { get; set; } 8 | public CommunityFiles Files { get; set; } 9 | public DateTimeOffset? UpdatedAt { get; set; } 10 | public bool ContentReportsEnabled { get; set; } 11 | 12 | public sealed class CommunityFiles 13 | { 14 | public CommunityFile CodeOfConductFile { get; set; } 15 | public CommunityFile Contributing { get; set; } 16 | public LicenseFile License { get; set; } 17 | public CommunityFile Readme { get; set; } 18 | } 19 | 20 | public class CommunityFile 21 | { 22 | public string Url { get; set; } 23 | public string HtmlUrl { get; set; } 24 | 25 | public string FileName => Path.GetFileName(new Uri(HtmlUrl).AbsolutePath); 26 | public string RawUrl 27 | { 28 | get 29 | { 30 | var builder = new UriBuilder(HtmlUrl) { Host = "raw.githubusercontent.com" }; 31 | builder.Path = builder.Path.Replace("/blob/", "/", StringComparison.Ordinal); 32 | return builder.ToString(); 33 | } 34 | } 35 | } 36 | 37 | public sealed class LicenseFile : CommunityFile 38 | { 39 | public string Key { get; set; } 40 | public string Name { get; set; } 41 | public string SpdxId { get; set; } 42 | public string NodeId { get; set; } 43 | } 44 | } 45 | 46 | #pragma warning restore CS8618 -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.GitHubCaching/Microsoft.DotnetOrg.GitHubCaching.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Ospo/GitHubInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.DotnetOrg.Ospo; 2 | #pragma warning disable CS8618 // This is a serialized type. 3 | public sealed class GitHubInfo 4 | { 5 | public long Id { get; set; } 6 | public string Login { get; set; } 7 | public List Organizations { get; set; } 8 | } 9 | #pragma warning restore CS8618 -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Ospo/Microsoft.DotnetOrg.Ospo.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Ospo/MicrosoftInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.DotnetOrg.Ospo; 2 | #pragma warning disable CS8618 // This is a serialized type. 3 | public sealed class MicrosoftInfo 4 | { 5 | public string Alias { get; set; } 6 | public string PreferredName { get; set; } 7 | public string UserPrincipalName { get; set; } 8 | public string EmailAddress { get; set; } 9 | public string Id { get; set; } 10 | } 11 | #pragma warning restore CS8618 -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Ospo/OspoClient.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Http.Headers; 3 | using System.Text.Json; 4 | 5 | namespace Microsoft.DotnetOrg.Ospo; 6 | 7 | public sealed class OspoClient : IDisposable 8 | { 9 | private readonly HttpClient _httpClient; 10 | 11 | public OspoClient(string token) 12 | { 13 | ArgumentNullException.ThrowIfNull(token); 14 | 15 | _httpClient = new HttpClient 16 | { 17 | BaseAddress = new Uri("https://repos.opensource.microsoft.com/api/") 18 | }; 19 | _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 20 | _httpClient.DefaultRequestHeaders.Add("api-version", "2019-10-01"); 21 | _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); 22 | } 23 | 24 | public void Dispose() 25 | { 26 | _httpClient.Dispose(); 27 | } 28 | 29 | public async Task GetAsync(string gitHubLogin) 30 | { 31 | var result = await GetAsJsonAsync($"people/links/github/{gitHubLogin}"); 32 | return result; 33 | } 34 | 35 | public async Task GetAllAsync() 36 | { 37 | var links = await GetAsJsonAsync>($"people/links"); 38 | 39 | var linkSet = new OspoLinkSet 40 | { 41 | Links = links ?? Array.Empty() 42 | }; 43 | 44 | linkSet.Initialize(); 45 | return linkSet; 46 | } 47 | 48 | private async Task GetAsJsonAsync(string requestUri) 49 | { 50 | var request = new HttpRequestMessage(HttpMethod.Get, requestUri); 51 | var response = await _httpClient.SendAsync(request); 52 | 53 | if (response.StatusCode == HttpStatusCode.NotFound) 54 | return default; 55 | 56 | if (!response.IsSuccessStatusCode) 57 | { 58 | var message = await response.Content.ReadAsStringAsync(); 59 | 60 | if (response.StatusCode == HttpStatusCode.Unauthorized) 61 | throw new OspoUnauthorizedException(message, response.StatusCode); 62 | 63 | throw new OspoException(message, response.StatusCode); 64 | } 65 | 66 | var responseStream = await response.Content.ReadAsStreamAsync(); 67 | 68 | var options = new JsonSerializerOptions 69 | { 70 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase 71 | }; 72 | 73 | return await JsonSerializer.DeserializeAsync(responseStream, options); 74 | } 75 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Ospo/OspoClientFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Text; 3 | 4 | namespace Microsoft.DotnetOrg.Ospo; 5 | 6 | public static class OspoClientFactory 7 | { 8 | public static async Task CreateAsync(string? token = null) 9 | { 10 | if (string.IsNullOrEmpty(token)) 11 | token = await GetOrCreateTokenAsync(); 12 | 13 | return new OspoClient(token); 14 | } 15 | 16 | private static async Task GetOrCreateTokenAsync() 17 | { 18 | var environmentToken = Environment.GetEnvironmentVariable("OSPOTOKEN"); 19 | if (!string.IsNullOrEmpty(environmentToken)) 20 | return environmentToken; 21 | 22 | string? token = null; 23 | 24 | var tokenFileName = GetTokenFileName(); 25 | if (File.Exists(tokenFileName)) 26 | { 27 | token = File.ReadAllText(tokenFileName).Trim(); 28 | if (!await IsValidAsync(token)) 29 | { 30 | Console.Error.WriteLine("error: OSPO token isn't valid anymore"); 31 | token = null; 32 | } 33 | } 34 | 35 | if (token is null) 36 | { 37 | token = await CreateTokenAsync(); 38 | var tokenFileDirectory = Path.GetDirectoryName(tokenFileName)!; 39 | Directory.CreateDirectory(tokenFileDirectory); 40 | File.WriteAllText(tokenFileName, token); 41 | } 42 | 43 | return token; 44 | } 45 | 46 | private static async Task IsValidAsync(string token) 47 | { 48 | var client = new OspoClient(token); 49 | try 50 | { 51 | await client.GetAsync("dotnet-bot"); 52 | return true; 53 | } 54 | catch (OspoUnauthorizedException) 55 | { 56 | return false; 57 | } 58 | } 59 | 60 | private static string GetExeName() 61 | { 62 | var exePath = Environment.GetCommandLineArgs()[0]; 63 | return Path.GetFileNameWithoutExtension(exePath); 64 | } 65 | 66 | private static string GetTokenFileName() 67 | { 68 | var exePath = Environment.GetCommandLineArgs()[0]; 69 | var fileInfo = FileVersionInfo.GetVersionInfo(exePath)!; 70 | var companyName = fileInfo.CompanyName ?? string.Empty; 71 | var productName = fileInfo.ProductName ?? string.Empty; 72 | return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), companyName, productName, "ospo-token.txt"); 73 | } 74 | 75 | private static async Task CreateTokenAsync() 76 | { 77 | var productName = GetExeName(); 78 | var url = "https://ossmsft.visualstudio.com/_usersSettings/tokens"; 79 | 80 | Console.WriteLine($"This is the first time you run {productName}."); 81 | Console.WriteLine($"{productName} needs to access the Open Source Program Office APIs."); 82 | Console.WriteLine(); 83 | Console.WriteLine($"Let's log you in so it can create a personal access token."); 84 | Console.WriteLine(); 85 | Console.WriteLine($"Press any key to navigate to {url}"); 86 | Console.WriteLine($"and create a token with the scope User Profil - Read."); 87 | 88 | Console.ReadKey(true); 89 | Console.WriteLine(); 90 | 91 | Process.Start(new ProcessStartInfo 92 | { 93 | FileName = url, 94 | UseShellExecute = true 95 | }); 96 | 97 | while (true) 98 | { 99 | Console.Write("Enter token: "); 100 | var token = ReadPassword(); 101 | 102 | var client = new OspoClient(token); 103 | 104 | try 105 | { 106 | await client.GetAsync("dotnet-bot"); 107 | return token; 108 | } 109 | catch (OspoUnauthorizedException) 110 | { 111 | Console.WriteLine($"error: invalid token"); 112 | } 113 | } 114 | } 115 | 116 | private static string ReadPassword() 117 | { 118 | var pwd = new StringBuilder(); 119 | while (true) 120 | { 121 | var i = Console.ReadKey(true); 122 | if (i.Key == ConsoleKey.Enter) 123 | { 124 | Console.WriteLine(); 125 | break; 126 | } 127 | else if (i.Key == ConsoleKey.Backspace) 128 | { 129 | if (pwd.Length > 0) 130 | { 131 | pwd.Remove(pwd.Length - 1, 1); 132 | Console.Write("\b \b"); 133 | } 134 | } 135 | else if (i.KeyChar != '\u0000') 136 | { 137 | pwd.Append(i.KeyChar); 138 | Console.Write("*"); 139 | } 140 | } 141 | return pwd.ToString(); 142 | } 143 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Ospo/OspoException.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Runtime.Serialization; 3 | 4 | namespace Microsoft.DotnetOrg.Ospo; 5 | 6 | public class OspoException : Exception 7 | { 8 | public OspoException() 9 | { 10 | } 11 | 12 | public OspoException(string? message) 13 | : base(message) 14 | { 15 | } 16 | 17 | public OspoException(string? message, Exception? inner) 18 | : base(message, inner) 19 | { 20 | } 21 | 22 | public OspoException(string? message, HttpStatusCode code) 23 | : this(message) 24 | { 25 | Code = code; 26 | } 27 | 28 | public HttpStatusCode Code { get; } 29 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Ospo/OspoLink.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Microsoft.DotnetOrg.Ospo; 4 | #pragma warning disable CS8618 // This is a serialized type. 5 | public sealed class OspoLink 6 | { 7 | [JsonPropertyName("github")] 8 | public GitHubInfo GitHubInfo { get; set; } 9 | 10 | [JsonPropertyName("aad")] 11 | public MicrosoftInfo MicrosoftInfo { get; set; } 12 | } 13 | #pragma warning restore CS8618 -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Ospo/OspoLinkSet.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Microsoft.DotnetOrg.Ospo; 4 | #pragma warning disable CS8618 // This is a serialized type. 5 | public sealed class OspoLinkSet 6 | { 7 | public OspoLinkSet() 8 | { 9 | } 10 | 11 | public void Initialize() 12 | { 13 | LinkByLogin = Links.ToDictionary(l => l.GitHubInfo.Login); 14 | } 15 | 16 | public IReadOnlyList Links { get; set; } = new List(); 17 | 18 | [JsonIgnore] 19 | public IReadOnlyDictionary LinkByLogin { get; set; } = new Dictionary(); 20 | } 21 | #pragma warning restore CS8618 -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Ospo/OspoUnauthorizedException.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Runtime.Serialization; 3 | 4 | namespace Microsoft.DotnetOrg.Ospo; 5 | 6 | public class OspoUnauthorizedException : OspoException 7 | { 8 | public OspoUnauthorizedException() 9 | { 10 | } 11 | 12 | public OspoUnauthorizedException(string? message) 13 | : base(message) 14 | { 15 | } 16 | 17 | public OspoUnauthorizedException(string? message, Exception? inner) 18 | : base(message, inner) 19 | { 20 | } 21 | 22 | public OspoUnauthorizedException(string? message, HttpStatusCode code) 23 | : base(message, code) 24 | { 25 | } 26 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29409.185 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "policop", "policop\policop.csproj", "{9A906FB6-F1DA-4789-9A1A-B5A0A45FA0A7}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Csv", "Microsoft.Csv\Microsoft.Csv.csproj", "{94F65290-CCE6-44D4-9107-1DB8046DC625}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Csv.Excel", "Microsoft.Csv.Excel\Microsoft.Csv.Excel.csproj", "{C245801A-88F8-415B-9D31-2123CF6B2DBD}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotnetOrg.GitHubCaching", "Microsoft.DotnetOrg.GitHubCaching\Microsoft.DotnetOrg.GitHubCaching.csproj", "{DA36972E-CBE7-4F5D-B0A1-00A98191039E}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotnetOrg.Ospo", "Microsoft.DotnetOrg.Ospo\Microsoft.DotnetOrg.Ospo.csproj", "{EFB69076-D1E6-4B48-B735-CE7FA2A71FE2}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BC72BAA5-6409-44D0-B0D8-846FB1952E75}" 17 | ProjectSection(SolutionItems) = preProject 18 | Directory.Build.props = Directory.Build.props 19 | EndProjectSection 20 | EndProject 21 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotnetOrg.Policies", "Microsoft.DotnetOrg.Policies\Microsoft.DotnetOrg.Policies.csproj", "{D9FC41ED-F8C9-409F-9036-28A7653E74FF}" 22 | EndProject 23 | Global 24 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 25 | Debug|Any CPU = Debug|Any CPU 26 | Release|Any CPU = Release|Any CPU 27 | EndGlobalSection 28 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 29 | {9A906FB6-F1DA-4789-9A1A-B5A0A45FA0A7}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {9A906FB6-F1DA-4789-9A1A-B5A0A45FA0A7}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {9A906FB6-F1DA-4789-9A1A-B5A0A45FA0A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {9A906FB6-F1DA-4789-9A1A-B5A0A45FA0A7}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {94F65290-CCE6-44D4-9107-1DB8046DC625}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {94F65290-CCE6-44D4-9107-1DB8046DC625}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {94F65290-CCE6-44D4-9107-1DB8046DC625}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {94F65290-CCE6-44D4-9107-1DB8046DC625}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {C245801A-88F8-415B-9D31-2123CF6B2DBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {C245801A-88F8-415B-9D31-2123CF6B2DBD}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {C245801A-88F8-415B-9D31-2123CF6B2DBD}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {C245801A-88F8-415B-9D31-2123CF6B2DBD}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {DA36972E-CBE7-4F5D-B0A1-00A98191039E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {DA36972E-CBE7-4F5D-B0A1-00A98191039E}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {DA36972E-CBE7-4F5D-B0A1-00A98191039E}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {DA36972E-CBE7-4F5D-B0A1-00A98191039E}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {EFB69076-D1E6-4B48-B735-CE7FA2A71FE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {EFB69076-D1E6-4B48-B735-CE7FA2A71FE2}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {EFB69076-D1E6-4B48-B735-CE7FA2A71FE2}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {EFB69076-D1E6-4B48-B735-CE7FA2A71FE2}.Release|Any CPU.Build.0 = Release|Any CPU 49 | {D9FC41ED-F8C9-409F-9036-28A7653E74FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {D9FC41ED-F8C9-409F-9036-28A7653E74FF}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {D9FC41ED-F8C9-409F-9036-28A7653E74FF}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {D9FC41ED-F8C9-409F-9036-28A7653E74FF}.Release|Any CPU.Build.0 = Release|Any CPU 53 | EndGlobalSection 54 | GlobalSection(SolutionProperties) = preSolution 55 | HideSolutionNode = FALSE 56 | EndGlobalSection 57 | GlobalSection(ExtensibilityGlobals) = postSolution 58 | SolutionGuid = {22574CCF-2C7C-482F-BD83-6324ACDFD3E4} 59 | EndGlobalSection 60 | EndGlobal 61 | -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/CodeOfConduct.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.DotnetOrg.Policies; 2 | 3 | internal static class CodeOfConduct 4 | { 5 | public static readonly string DotNetFoundationLink = "https://dotnetfoundation.org/code-of-conduct"; 6 | public static readonly string MicrosoftLink = "https://opensource.microsoft.com/codeofconduct"; 7 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/Microsoft.DotnetOrg.Policies.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/PolicyAnalysisContext.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using Microsoft.DotnetOrg.GitHubCaching; 3 | 4 | namespace Microsoft.DotnetOrg.Policies; 5 | 6 | public sealed class PolicyAnalysisContext 7 | { 8 | private readonly ConcurrentBag _violations = new ConcurrentBag(); 9 | 10 | public PolicyAnalysisContext(CachedOrg org) 11 | { 12 | ArgumentNullException.ThrowIfNull(org); 13 | 14 | Org = org; 15 | } 16 | 17 | public CachedOrg Org { get; } 18 | 19 | public void ReportViolation(PolicyDescriptor descriptor, 20 | string title, 21 | string body, 22 | CachedRepo? repo = null, 23 | CachedSecret? secret = null, 24 | CachedBranch? branch = null, 25 | CachedTeam? team = null, 26 | CachedUser? user = null, 27 | IReadOnlyCollection? assignees = null) 28 | { 29 | var violation = new PolicyViolation(descriptor, 30 | title, 31 | body, 32 | Org, 33 | repo, 34 | secret, 35 | branch, 36 | team, 37 | user, 38 | assignees); 39 | _violations.Add(violation); 40 | } 41 | 42 | public IReadOnlyList GetViolations() 43 | { 44 | return _violations.ToArray(); 45 | } 46 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/PolicyDescriptor.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.DotnetOrg.Policies; 2 | 3 | public sealed class PolicyDescriptor 4 | { 5 | public PolicyDescriptor(string diagnosticId, string title, PolicySeverity severity) 6 | { 7 | if (string.IsNullOrEmpty(diagnosticId)) 8 | throw new ArgumentException($"'{nameof(diagnosticId)}' cannot be null or empty.", nameof(diagnosticId)); 9 | 10 | if (string.IsNullOrEmpty(title)) 11 | throw new ArgumentException($"'{nameof(title)}' cannot be null or empty.", nameof(title)); 12 | 13 | DiagnosticId = diagnosticId; 14 | Title = title; 15 | Severity = severity; 16 | } 17 | 18 | public string DiagnosticId { get; } 19 | public string Title { get; } 20 | public PolicySeverity Severity { get; } 21 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/PolicyRule.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.DotnetOrg.Policies; 2 | 3 | public abstract class PolicyRule 4 | { 5 | public abstract PolicyDescriptor Descriptor { get; } 6 | 7 | public virtual void GetViolations(PolicyAnalysisContext context) 8 | { 9 | } 10 | 11 | public virtual Task GetViolationsAsync(PolicyAnalysisContext context) 12 | { 13 | GetViolations(context); 14 | return Task.CompletedTask; 15 | } 16 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/PolicyRunner.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.DotnetOrg.Policies; 2 | 3 | public static class PolicyRunner 4 | { 5 | public static IReadOnlyList GetRules() 6 | { 7 | return typeof(PolicyRunner).Assembly 8 | .GetTypes() 9 | .Where(t => !t.IsAbstract && 10 | t.GetConstructor(Array.Empty()) is not null && 11 | typeof(PolicyRule).IsAssignableFrom(t)) 12 | .Select(t => Activator.CreateInstance(t)) 13 | .Cast() 14 | .ToList(); 15 | } 16 | 17 | public static Task RunAsync(PolicyAnalysisContext context) 18 | { 19 | var rules = GetRules().Where(r => r.Descriptor.Severity > PolicySeverity.Hidden); 20 | return RunAsync(context, rules); 21 | } 22 | 23 | public static Task RunAsync(PolicyAnalysisContext context, IEnumerable rules) 24 | { 25 | var ruleTasks = rules.Select(r => r.GetViolationsAsync(context)); 26 | return Task.WhenAll(ruleTasks); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/PolicySeverity.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.DotnetOrg.Policies; 2 | 3 | public enum PolicySeverity 4 | { 5 | Hidden, 6 | Warning, 7 | Error 8 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/Rules/PR01_MicrosoftEmployeesShouldBeLinked.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.DotnetOrg.Policies.Rules; 2 | 3 | internal sealed class PR01_MicrosoftEmployeesShouldBeLinked : PolicyRule 4 | { 5 | public override PolicyDescriptor Descriptor { get; } = new PolicyDescriptor( 6 | "PR01", 7 | "Microsoft employees should be linked", 8 | PolicySeverity.Error 9 | ); 10 | 11 | public override void GetViolations(PolicyAnalysisContext context) 12 | { 13 | foreach (var user in context.Org.Users) 14 | { 15 | var userClaimsToBeWorkingForMicrosoft = user.IsClaimingToBeWorkingForMicrosoft(); 16 | var isMicrosoftUser = user.IsMicrosoftUser(); 17 | 18 | if (userClaimsToBeWorkingForMicrosoft && !isMicrosoftUser) 19 | { 20 | context.ReportViolation( 21 | Descriptor, 22 | $"Microsoft-user '{user.Login}' should be linked", 23 | $@" 24 | User {user.Markdown()} appears to be a Microsoft employee. They should be [linked](https://opensource.microsoft.com/link) to a Microsoft account. 25 | 26 | For more details, see [documentation](https://docs.opensource.microsoft.com/tools/github/accounts/linking.html). 27 | ", 28 | user: user 29 | ); 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/Rules/PR02_MicrosoftOwnedRepoShouldAtMostGrantTriageToExternals.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | namespace Microsoft.DotnetOrg.Policies.Rules; 4 | 5 | internal sealed class PR02_MicrosoftOwnedRepoShouldAtMostGrantTriageToExternals : PolicyRule 6 | { 7 | private const CachedPermission _maxPermission = CachedPermission.Triage; 8 | 9 | public override PolicyDescriptor Descriptor { get; } = new PolicyDescriptor( 10 | "PR02", 11 | $"Microsoft-owned repo should at most grant '{_maxPermission.ToString().ToLower()}' to externals", 12 | PolicySeverity.Error 13 | ); 14 | 15 | public override void GetViolations(PolicyAnalysisContext context) 16 | { 17 | foreach (var repo in context.Org.Repos) 18 | { 19 | var isRepoOwnedByMicrosoft = repo.IsOwnedByMicrosoft(); 20 | if (isRepoOwnedByMicrosoft) 21 | { 22 | foreach (var userAccess in repo.Users) 23 | { 24 | var user = userAccess.User; 25 | var permission = userAccess.Permission; 26 | var userWorksForMicrosoft = user.IsMicrosoftUser(); 27 | if (!userWorksForMicrosoft && permission > _maxPermission) 28 | { 29 | context.ReportViolation( 30 | Descriptor, 31 | title: $"Non-Microsoft contributor '{user.Login}' should at most have '{_maxPermission.ToString().ToLower()}' permission for '{repo.Name}'", 32 | body: $@" 33 | The non-Microsoft contributor {user.Markdown()} was granted {permission.Markdown()} for the Microsoft-owned repo {repo.Markdown()}. 34 | 35 | Only Microsoft users should have more than {_maxPermission.Markdown()} permissions. 36 | 37 | * If this is a Microsoft user, they need to [link](https://docs.opensource.microsoft.com/tools/github/accounts/linking.html) their account. 38 | * If this isn't a Microsoft user, their permission needs to be changed to `triage`. 39 | ", 40 | repo: repo, 41 | user: user 42 | ); 43 | } 44 | } 45 | } 46 | }; 47 | } 48 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/Rules/PR03_MicrosoftOwnedTeamShouldOnlyContainEmployees.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.DotnetOrg.Policies.Rules; 2 | 3 | internal sealed class PR03_MicrosoftOwnedTeamShouldOnlyContainEmployees : PolicyRule 4 | { 5 | public override PolicyDescriptor Descriptor { get; } = new PolicyDescriptor( 6 | "PR03", 7 | "Microsoft-owned team should only contain Microsoft users", 8 | PolicySeverity.Error 9 | ); 10 | 11 | public override void GetViolations(PolicyAnalysisContext context) 12 | { 13 | foreach (var team in context.Org.Teams) 14 | { 15 | var isOwnedByMicrosoft = team.IsOwnedByMicrosoft(); 16 | if (isOwnedByMicrosoft) 17 | { 18 | foreach (var user in team.Members) 19 | { 20 | var isMicrosoftUser = user.IsMicrosoftUser(); 21 | if (!isMicrosoftUser) 22 | { 23 | context.ReportViolation( 24 | Descriptor, 25 | $"Microsoft owned team '{team.Name}' shouldn't contain '{user.Login}'", 26 | $@" 27 | Microsoft owned team {team.Markdown()} shouldn't contain user {user.Markdown()} because they are not an employee. 28 | 29 | * If this is a Microsoft user, they need to [link](https://docs.opensource.microsoft.com/tools/github/accounts/linking.html) their account. 30 | * If this team is supposed to represent Microsoft and non-Microsoft, the team shouldn't be owned by Microsoft 31 | * If this isn't a Microsoft user, they need to be removed from this team. 32 | ", 33 | team: team, 34 | user: user 35 | ); 36 | } 37 | } 38 | } 39 | }; 40 | } 41 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/Rules/PR04_MicrosoftTeamShouldBeMarkedAsOwnedByMicrosoft.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | namespace Microsoft.DotnetOrg.Policies.Rules; 4 | 5 | internal sealed class PR04_MicrosoftTeamShouldBeMarkedAsOwnedByMicrosoft : PolicyRule 6 | { 7 | public override PolicyDescriptor Descriptor { get; } = new PolicyDescriptor( 8 | "PR04", 9 | "Team should be owned by Microsoft", 10 | PolicySeverity.Error 11 | ); 12 | 13 | public override void GetViolations(PolicyAnalysisContext context) 14 | { 15 | var maxNonMicrosoftPermission = CachedPermission.Triage; 16 | var microsoftTeam = context.Org.GetMicrosoftTeam(); 17 | var microsoftTeamMarkdown = microsoftTeam?.Markdown() ?? "Microsoft"; 18 | 19 | foreach (var team in context.Org.Teams) 20 | { 21 | var isOwnedByMicrosoft = team.IsOwnedByMicrosoft(); 22 | var exceedsMaxForExternals = team.Repos.Any(r => r.Repo.IsOwnedByMicrosoft() && r.Permission > maxNonMicrosoftPermission); 23 | 24 | if (!isOwnedByMicrosoft && exceedsMaxForExternals) 25 | { 26 | context.ReportViolation( 27 | Descriptor, 28 | $"Team '{team.Name}' must be owned by Microsoft", 29 | $@" 30 | Team {team.Markdown()} grants at least one Microsoft-owned repo more than {maxNonMicrosoftPermission.Markdown()} permissions. The team must be owned by Microsoft. 31 | 32 | To indicate that the team is owned by Microsoft, ensure that one of the parent teams is {microsoftTeamMarkdown}. 33 | ", 34 | team: team 35 | ); 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/Rules/PR05_MarkerTeamShouldOnlyGrantReadAccess.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | namespace Microsoft.DotnetOrg.Policies.Rules; 4 | 5 | internal sealed class PR05_MarkerTeamShouldOnlyGrantReadAccess : PolicyRule 6 | { 7 | public override PolicyDescriptor Descriptor { get; } = new PolicyDescriptor( 8 | "PR05", 9 | "Marker team should only grant 'read' access", 10 | PolicySeverity.Error 11 | ); 12 | 13 | public override void GetViolations(PolicyAnalysisContext context) 14 | { 15 | foreach (var repo in context.Org.Repos) 16 | { 17 | foreach (var teamAccess in repo.Teams) 18 | { 19 | var team = teamAccess.Team; 20 | if (team.IsMarkerTeam() && 21 | teamAccess.Permission != CachedPermission.Read) 22 | { 23 | context.ReportViolation( 24 | Descriptor, 25 | $"Repo '{repo.Name}' should only grant '{team.Name}' with 'read' permissions", 26 | $@" 27 | The marker team {team.Markdown()} is only used to indicate ownership. It should only ever grant `read` permissions. 28 | 29 | Change the permissions for {team.Markdown()} in repo {repo.Markdown()} to `read`. 30 | ", 31 | repo: repo, 32 | team: team 33 | ); 34 | } 35 | } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/Rules/PR06_InactiveReposShouldBeArchived.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.DotnetOrg.Policies.Rules; 2 | 3 | internal sealed class PR06_InactiveReposShouldBeArchived : PolicyRule 4 | { 5 | public override PolicyDescriptor Descriptor { get; } = new PolicyDescriptor( 6 | "PR06", 7 | "Inactive repos should be archived", 8 | PolicySeverity.Warning 9 | ); 10 | 11 | public override void GetViolations(PolicyAnalysisContext context) 12 | { 13 | var now = DateTimeOffset.Now; 14 | var threshold = TimeSpan.FromDays(365); 15 | 16 | foreach (var repo in context.Org.Repos) 17 | { 18 | var alreadyArchived = repo.IsArchived; 19 | var inactivity = now - repo.LastPush; 20 | if (!alreadyArchived && inactivity > threshold) 21 | { 22 | context.ReportViolation( 23 | Descriptor, 24 | title: $"Inactive repo '{repo.Name}' should be archived", 25 | body: $@" 26 | The last push to repo {repo.Markdown()} is more than {threshold.TotalDays:N0} days ago. It should be archived. 27 | ", 28 | repo: repo 29 | ); 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/Rules/PR07_UnusedTeamShouldNotExist.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.DotnetOrg.Policies.Rules; 2 | 3 | internal sealed class PR07_UnusedTeamShouldNotExist : PolicyRule 4 | { 5 | public override PolicyDescriptor Descriptor { get; } = new PolicyDescriptor( 6 | "PR07", 7 | "Unused team should be removed", 8 | PolicySeverity.Warning 9 | ); 10 | 11 | public override void GetViolations(PolicyAnalysisContext context) 12 | { 13 | foreach (var team in context.Org.Teams) 14 | { 15 | if (team.IsUnused()) 16 | { 17 | context.ReportViolation( 18 | Descriptor, 19 | $"Unused team '{team.Name}' should be removed", 20 | $@" 21 | Team {team.Markdown()} doesn't have any associated repos nor nested teams. It should either be used or removed. 22 | ", 23 | team: team 24 | ); 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/Rules/PR08_TooManyRepoAdmins.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.DotnetOrg.Policies.Rules; 2 | 3 | internal sealed class PR08_TooManyRepoAdmins : PolicyRule 4 | { 5 | public override PolicyDescriptor Descriptor { get; } = new PolicyDescriptor( 6 | "PR08", 7 | "Too many repo admins", 8 | PolicySeverity.Error 9 | ); 10 | 11 | public override void GetViolations(PolicyAnalysisContext context) 12 | { 13 | const int Threshold = 10; 14 | 15 | foreach (var repo in context.Org.Repos) 16 | { 17 | if (repo.IsArchived) 18 | continue; 19 | 20 | // Note: Even if we don't fallback to owners, we don't want owners to 21 | // to count towards the admin quota. 22 | var numberOfAdmins = repo.GetAdministrators(fallbackToOwners: false) 23 | .Count(u => !u.IsOwner && !u.IsBot()); 24 | 25 | if (numberOfAdmins > Threshold) 26 | { 27 | context.ReportViolation( 28 | Descriptor, 29 | $"Repo '{repo.Name}' has too many admins", 30 | $@" 31 | The repo {repo.Markdown()} has {numberOfAdmins} admins. Reduce the number of admins to {Threshold} or less. 32 | ", 33 | repo: repo 34 | ); 35 | } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/Rules/PR09_TooManyTeamMaintainers.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.DotnetOrg.Policies.Rules; 2 | 3 | internal sealed class PR09_TooManyTeamMaintainers : PolicyRule 4 | { 5 | public override PolicyDescriptor Descriptor { get; } = new PolicyDescriptor( 6 | "PR09", 7 | "Too many team maintainers", 8 | PolicySeverity.Error 9 | ); 10 | 11 | public override void GetViolations(PolicyAnalysisContext context) 12 | { 13 | const int Threshold = 10; 14 | 15 | foreach (var team in context.Org.Teams) 16 | { 17 | var numberOfMaintainers = team.Maintainers.Count(m => !m.IsOwner); 18 | 19 | if (numberOfMaintainers > Threshold) 20 | { 21 | context.ReportViolation( 22 | Descriptor, 23 | $"Team '{team.Name}' has too many maintainers", 24 | $@" 25 | The team {team.Markdown()} has {numberOfMaintainers} maintainers. Reduce the number of maintainers to {Threshold} or less. 26 | ", 27 | team: team 28 | ); 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/Rules/PR10_AdminsShouldBeInTeams.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | namespace Microsoft.DotnetOrg.Policies.Rules; 4 | 5 | internal sealed class PR10_AdminsShouldBeInTeams : PolicyRule 6 | { 7 | public override PolicyDescriptor Descriptor { get; } = new PolicyDescriptor( 8 | "PR10", 9 | "Admins should be in teams", 10 | PolicySeverity.Error 11 | ); 12 | 13 | public override void GetViolations(PolicyAnalysisContext context) 14 | { 15 | foreach (var repo in context.Org.Repos) 16 | { 17 | if (repo.IsArchived || !repo.IsOwnedByMicrosoft()) 18 | continue; 19 | 20 | var adminTeams = repo.Teams.Where(ta => ta.Permission == CachedPermission.Admin) 21 | .Select(ta => ta.Team); 22 | 23 | var recommendation = string.Join(", ", adminTeams.Select(a => a.Markdown())); 24 | 25 | if (recommendation.Length > 0) 26 | recommendation = "Consider adding the user to one of these teams: " + recommendation + "."; 27 | else 28 | recommendation = "Grant a team `admin` permissions for this repo and ensure the user is in that team."; 29 | 30 | foreach (var userAccess in repo.Users) 31 | { 32 | var user = userAccess.User; 33 | var isAdmin = userAccess.Permission == CachedPermission.Admin; 34 | 35 | if (isAdmin) 36 | { 37 | context.ReportViolation( 38 | Descriptor, 39 | $"Admin access for '{user.Login}' in repo '{repo.Name}' should be granted via a team", 40 | $@" 41 | The user {user.Markdown()} shouldn't be directly added as an admin for repo {repo.Markdown()}. Instead, the user should be in a team that is granted `admin` permissions. 42 | 43 | {recommendation} 44 | ", 45 | repo: repo, 46 | user: user 47 | ); 48 | } 49 | } 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/Rules/PR11_ReposShouldHaveSufficientNumberOfAdmins.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.DotnetOrg.Policies.Rules; 2 | 3 | internal sealed class PR11_ReposShouldHaveSufficientNumberOfAdmins : PolicyRule 4 | { 5 | public override PolicyDescriptor Descriptor { get; } = new PolicyDescriptor( 6 | "PR11", 7 | "Repos should have a sufficient number of admins", 8 | PolicySeverity.Warning 9 | ); 10 | 11 | public override void GetViolations(PolicyAnalysisContext context) 12 | { 13 | const int Threshold = 2; 14 | foreach (var repo in context.Org.Repos) 15 | { 16 | if (!repo.IsOwnedByMicrosoft()) 17 | continue; 18 | 19 | var isArchived = repo.IsArchived; 20 | var numberOfAdmins = repo.GetAdministrators(fallbackToOwners: false).Count(); 21 | 22 | if (!isArchived && numberOfAdmins < Threshold) 23 | { 24 | context.ReportViolation( 25 | Descriptor, 26 | $"Repo '{repo.Name}' needs more admins", 27 | $@" 28 | The repo {repo.Markdown()} has {numberOfAdmins} admins (excluding organization owners). It is recommended to have at least {Threshold} admins. 29 | ", 30 | repo: repo 31 | ); 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/Rules/PR12_BotsShouldBeInTheBotsTeam.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.DotnetOrg.Policies.Rules; 2 | 3 | internal sealed class PR12_BotsShouldBeInTheBotsTeam : PolicyRule 4 | { 5 | public override PolicyDescriptor Descriptor { get; } = new PolicyDescriptor( 6 | "PR12", 7 | "Bots should be in the 'bots' team", 8 | PolicySeverity.Warning 9 | ); 10 | 11 | public override void GetViolations(PolicyAnalysisContext context) 12 | { 13 | var botsTeam = context.Org.GetBotsTeam(); 14 | if (botsTeam is null) 15 | return; 16 | 17 | foreach (var user in context.Org.Users) 18 | { 19 | var isKnownBot = user.IsBot(); 20 | var isPotentiallyABot = user.IsPotentiallyABot(); 21 | if (!isKnownBot && isPotentiallyABot) 22 | { 23 | context.ReportViolation( 24 | Descriptor, 25 | $"User '{user.Login}' should be marked as a bot", 26 | $@" 27 | The user {user.Markdown()} appears to be a bot. 28 | 29 | * If this is in fact a human, mark this issue as `policy-override` and close the issue 30 | * If this is a bot, add it to the team {botsTeam.Markdown()} 31 | ", 32 | team: botsTeam, 33 | user: user 34 | ); 35 | } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/Rules/PR13_CollaboratorAccessIsSuperfluous.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | namespace Microsoft.DotnetOrg.Policies.Rules; 4 | 5 | internal sealed class PR13_CollaboratorAccessIsSuperfluous : PolicyRule 6 | { 7 | public override PolicyDescriptor Descriptor { get; } = new PolicyDescriptor( 8 | "PR13", 9 | "Collaborator access is superfluous", 10 | PolicySeverity.Warning 11 | ); 12 | 13 | public override void GetViolations(PolicyAnalysisContext context) 14 | { 15 | foreach (var repo in context.Org.Repos) 16 | { 17 | if (repo.IsArchived) 18 | continue; 19 | 20 | var orgOwnersOrTeamUsers = new Dictionary(); 21 | 22 | foreach (var teamAccess in repo.Teams) 23 | { 24 | foreach (var user in teamAccess.Team.EffectiveMembers) 25 | { 26 | if (orgOwnersOrTeamUsers.TryGetValue(user, out var userAccess)) 27 | { 28 | if (userAccess.Permission >= teamAccess.Permission) 29 | continue; 30 | } 31 | 32 | orgOwnersOrTeamUsers[user] = new CachedUserAccess 33 | { 34 | Repo = repo, 35 | RepoName = repo.Name, 36 | User = user, 37 | UserLogin = user.Login, 38 | Permission = teamAccess.Permission 39 | }; 40 | } 41 | } 42 | 43 | foreach (var collaboratorAccess in repo.Users) 44 | { 45 | var user = collaboratorAccess.User; 46 | var permission = collaboratorAccess.Permission; 47 | 48 | if (user.IsOwner) 49 | { 50 | // We want owner rules to only apply to Microsoft repos. The reason being that 51 | // if the owner status is removed, the permissions for the repo should remain. 52 | // This generally doesn't apply to Microsoft-owned repos. 53 | if (repo.IsOwnedByMicrosoft()) 54 | { 55 | context.ReportViolation( 56 | Descriptor, 57 | $"Collaborator access for user '{user.Login}' is superfluous", 58 | $@" 59 | In repo {repo.Markdown()} the user {user.Markdown()} was granted {permission.Markdown()} as a collaborator but the user is an organization owner. 60 | 61 | You should remove the collaborator access. 62 | ", 63 | repo: repo, 64 | user: user 65 | ); 66 | } 67 | } 68 | else if (orgOwnersOrTeamUsers.TryGetValue(user, out var teamUserAccess) && 69 | permission <= teamUserAccess.Permission) 70 | { 71 | var teamPermission = teamUserAccess.Permission; 72 | var teams = repo.Teams.Where(ta => ta.Permission == teamUserAccess.Permission && 73 | ta.Team.EffectiveMembers.Contains(user)) 74 | .Select(ta => ta.Team.Markdown()); 75 | 76 | var teamListMarkdown = string.Join(", ", teams); 77 | context.ReportViolation( 78 | Descriptor, 79 | $"Collaborator access for user '{user.Login}' is superfluous", 80 | $@" 81 | In repo {repo.Markdown()} the user {user.Markdown()} was granted {permission.Markdown()} as a collaborator but the user already has {teamPermission.Markdown()} permissions via the team(s) {teamListMarkdown}. 82 | 83 | You should remove the collaborator access. 84 | ", 85 | repo: repo, 86 | user: user 87 | ); 88 | } 89 | } 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/Rules/PR14_RepoOwnershipMustBeExplicit.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | namespace Microsoft.DotnetOrg.Policies.Rules; 4 | 5 | internal sealed class PR14_RepoOwnershipMustBeExplicit : PolicyRule 6 | { 7 | public override PolicyDescriptor Descriptor { get; } = new PolicyDescriptor( 8 | "PR14", 9 | "Repo ownership must be explicit", 10 | PolicySeverity.Error 11 | ); 12 | 13 | public override void GetViolations(PolicyAnalysisContext context) 14 | { 15 | // If the entire org is owned by Microsoft, we don't need explicit ownership 16 | if (context.Org.IsOwnedByMicrosoft()) 17 | return; 18 | 19 | var microsoftTeam = context.Org.GetMicrosoftTeam(); 20 | var nonMicrosoftTeam = context.Org.GetNonMicrosoftTeam(); 21 | 22 | if (microsoftTeam is null || nonMicrosoftTeam is null) 23 | return; 24 | 25 | foreach (var repo in context.Org.Repos) 26 | { 27 | if (repo.IsArchived) 28 | continue; 29 | 30 | // These repos don't live long. There is no point in enforcing ownership semantics. 31 | if (repo.IsTemporaryForkForSecurityAdvisory()) 32 | continue; 33 | 34 | var microsoftTeamIsAssigned = repo.Teams.Any(ta => ta.Team == microsoftTeam); 35 | var nonMicrosoftTeamIsAssigned = repo.Teams.Any(ta => ta.Team == nonMicrosoftTeam); 36 | 37 | var isExplicitlyMarked = microsoftTeamIsAssigned || nonMicrosoftTeamIsAssigned; 38 | 39 | if (!isExplicitlyMarked) 40 | { 41 | var permission = CachedPermission.Read; 42 | 43 | context.ReportViolation( 44 | Descriptor, 45 | $"Repo '{repo.Name}' must indicate ownership", 46 | $@" 47 | The repo {repo.Markdown()} needs to indicate whether it's owned by Microsoft. 48 | 49 | * **Owned by Microsoft**. Assign the team {microsoftTeam.Markdown()} with {permission.Markdown()} permissions. 50 | * **Not owned by Microsoft**. Assign the team {nonMicrosoftTeam.Markdown()} with {permission.Markdown()} permissions. 51 | ", 52 | repo: repo 53 | ); 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/Rules/PR15_RepoMustHaveACodeOfConduct.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.DotnetOrg.Policies.Rules; 2 | 3 | internal sealed class PR15_RepoMustHaveACodeOfConduct : PolicyRule 4 | { 5 | public override PolicyDescriptor Descriptor { get; } = new PolicyDescriptor( 6 | "PR15", 7 | "Repo must have a Code of Conduct", 8 | PolicySeverity.Error 9 | ); 10 | 11 | public override void GetViolations(PolicyAnalysisContext context) 12 | { 13 | if (!string.Equals(context.Org.Name, "aspnet", StringComparison.OrdinalIgnoreCase) && 14 | !string.Equals(context.Org.Name, "dotnet", StringComparison.OrdinalIgnoreCase) && 15 | !string.Equals(context.Org.Name, "mono", StringComparison.OrdinalIgnoreCase)) 16 | return; 17 | 18 | // This rule is not scoped to anyone because both Microsoft and .NET Foundation 19 | // projects are expected to follow this policy. 20 | 21 | foreach (var repo in context.Org.Repos) 22 | { 23 | if (repo.IsPrivate || repo.IsArchivedOrSoftArchived()) 24 | continue; 25 | 26 | // Let's also exclude forks and mirrors because adding a file to 27 | // these repos isn't always practical (as they usually belong to 28 | // other communities). 29 | if (repo.IsFork || repo.IsMirror) 30 | continue; 31 | 32 | if (repo.CodeOfConduct is not null) 33 | continue; 34 | 35 | context.ReportViolation( 36 | Descriptor, 37 | $"Repo '{repo.Name}' must have a Code of Conduct", 38 | $@" 39 | The repo {repo.Markdown()} needs to include a file that links to the Code of Conduct. 40 | 41 | For more details, see [PR15](https://github.com/dotnet/org-policy/blob/main/doc/PR15.md). 42 | ", 43 | repo: repo 44 | ); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/Rules/PR16_ReposMustLinkCorrectCodeOfConduct.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | namespace Microsoft.DotnetOrg.Policies.Rules; 4 | 5 | internal sealed class PR16_ReposMustLinkCorrectCodeOfConduct: PolicyRule 6 | { 7 | public override PolicyDescriptor Descriptor { get; } = new PolicyDescriptor( 8 | "PR16", 9 | "Repos must link correct Code of Conduct", 10 | PolicySeverity.Error 11 | ); 12 | 13 | public override void GetViolations(PolicyAnalysisContext context) 14 | { 15 | // TODO: Enable for other orgs 16 | // mono 17 | if (!string.Equals(context.Org.Name, "aspnet", StringComparison.OrdinalIgnoreCase) && 18 | !string.Equals(context.Org.Name, "dotnet", StringComparison.OrdinalIgnoreCase)) 19 | return; 20 | 21 | var probedFoundationCoCReferences = new[] 22 | { 23 | "http://dotnetfoundation.org/code-of-conduct", 24 | "https://dotnetfoundation.org/code-of-conduct" 25 | }; 26 | 27 | var probedMicrosoftCocReferences = new[] 28 | { 29 | "http://opensource.microsoft.com/codeofconduct", 30 | "https://opensource.microsoft.com/codeofconduct", 31 | "opencode@microsoft.com" 32 | }; 33 | 34 | var problematicFiles = new HashSet<(string Name, string Url)>(); 35 | 36 | foreach (var repo in context.Org.Repos) 37 | { 38 | if (repo.IsPrivate || repo.IsArchivedOrSoftArchived()) 39 | continue; 40 | 41 | // Let's also exclude forks and mirrors because adding a file to 42 | // these repos isn't always practical (as they usually belong to 43 | // other communities). 44 | if (repo.IsFork || repo.IsMirror) 45 | continue; 46 | 47 | var kind = repo.IsUnderDotNetFoundation() 48 | ? ".NET Foundation" 49 | : "Microsoft"; 50 | 51 | var expectedLink = repo.IsUnderDotNetFoundation() 52 | ? CodeOfConduct.DotNetFoundationLink 53 | : CodeOfConduct.MicrosoftLink; 54 | 55 | var problematicReferences = repo.IsUnderDotNetFoundation() 56 | ? probedMicrosoftCocReferences 57 | : probedFoundationCoCReferences; 58 | 59 | problematicFiles.Clear(); 60 | 61 | // First check that the CoC links the expected CoC 62 | 63 | if (repo.CodeOfConduct is not null) 64 | { 65 | var containsExpectedLink = repo.CodeOfConduct.Contents.Contains(expectedLink, StringComparison.OrdinalIgnoreCase); 66 | if (!containsExpectedLink) 67 | problematicFiles.Add((repo.CodeOfConduct.Name, repo.CodeOfConduct.Url)); 68 | } 69 | 70 | void CheckForProblematicReferences(CachedFile? file) 71 | { 72 | if (file is null) 73 | return; 74 | 75 | var containsProblematicReference = problematicReferences.Any(t => file.Contents.Contains(t, StringComparison.OrdinalIgnoreCase)); 76 | if (containsProblematicReference) 77 | problematicFiles.Add((file.Name, file.Url)); 78 | } 79 | 80 | CheckForProblematicReferences(repo.CodeOfConduct); 81 | CheckForProblematicReferences(repo.ReadMe); 82 | CheckForProblematicReferences(repo.Contributing); 83 | 84 | if (!problematicFiles.Any()) 85 | continue; 86 | 87 | var repoUrl = repo.Url; 88 | var linkedFileList = string.Join(", ", problematicFiles.OrderBy(t => t.Name).Select(f => $"[{f.Name}]({f.Url})")); 89 | 90 | context.ReportViolation( 91 | Descriptor, 92 | $"Repo '{repo.Name}' must link correct Code of Conduct", 93 | $@" 94 | The repo {repo.Markdown()} is a {kind} project. Thus, it should only reference the {kind} Code of Conduct. 95 | 96 | For more details, see [PR15](https://github.com/dotnet/org-policy/blob/main/doc/PR15.md). 97 | 98 | Affected files: {linkedFileList} 99 | ", 100 | repo: repo 101 | ); 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/Rules/PR17_TeamsShouldHaveSufficientNumberOfMaintainers.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.DotnetOrg.Policies.Rules; 2 | 3 | internal sealed class PR17_TeamsShouldHaveSufficientNumberOfMaintainers : PolicyRule 4 | { 5 | public override PolicyDescriptor Descriptor { get; } = new PolicyDescriptor( 6 | "PR17", 7 | "Teams should have a sufficient number of maintainers", 8 | PolicySeverity.Warning 9 | ); 10 | 11 | public override void GetViolations(PolicyAnalysisContext context) 12 | { 13 | const int Threshold = 2; 14 | foreach (var team in context.Org.Teams) 15 | { 16 | if (!team.IsOwnedByMicrosoft()) 17 | continue; 18 | 19 | var teamThreshold = Math.Min(Threshold, team.Members.Count); 20 | var numberOfMaintainers = team.GetMaintainers().Count(); 21 | 22 | if (numberOfMaintainers < teamThreshold) 23 | { 24 | context.ReportViolation( 25 | Descriptor, 26 | $"Team '{team.Name}' needs more maintainers", 27 | $@" 28 | The team {team.Markdown()} has {numberOfMaintainers} maintainers. It should have at least {teamThreshold} maintainers. 29 | ", 30 | team: team 31 | ); 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/Rules/PR18_ReposShouldNotUseDeprecatedBranchNames.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.DotnetOrg.Policies.Rules; 2 | 3 | internal sealed class PR18_ReposShouldNotUseDeprecatedBranchNames : PolicyRule 4 | { 5 | public override PolicyDescriptor Descriptor { get; } = new PolicyDescriptor( 6 | "PR18", 7 | "Repos shouldn't use deprecated branch names", 8 | PolicySeverity.Warning 9 | ); 10 | 11 | public override void GetViolations(PolicyAnalysisContext context) 12 | { 13 | var deprecatedBranchNames = new (string Deprecated, string Preferred)[] 14 | { 15 | ("master", "main") 16 | }; 17 | 18 | foreach (var repo in context.Org.Repos) 19 | { 20 | if (repo.IsArchivedOrSoftArchived()) 21 | continue; 22 | 23 | if (repo.IsFork) 24 | continue; 25 | 26 | if (!repo.IsOwnedByMicrosoft()) 27 | continue; 28 | 29 | foreach (var branch in repo.Branches) 30 | { 31 | foreach (var (deprecatedName, preferredName) in deprecatedBranchNames) 32 | { 33 | if (branch.Name.Contains(deprecatedName, StringComparison.OrdinalIgnoreCase)) 34 | { 35 | context.ReportViolation( 36 | Descriptor, 37 | $"Repo '{repo.Name}' uses deprecated branch name '{branch.Name}'", 38 | $@" 39 | The repo {repo.Markdown()} contains the branch {branch.Markdown()} which contains the deprecated branch name '{deprecatedName}'. It should use the name '{preferredName}'. 40 | ", 41 | repo: repo, 42 | branch: branch 43 | ); 44 | } 45 | } 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/Rules/PR19_DefaultBranchesShouldBeProtected.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.DotnetOrg.Policies.Rules; 2 | 3 | internal sealed class PR19_DefaultBranchesShouldBeProtected : PolicyRule 4 | { 5 | public override PolicyDescriptor Descriptor { get; } = new PolicyDescriptor( 6 | "PR19", 7 | "Default branches should have branch protection", 8 | PolicySeverity.Warning 9 | ); 10 | 11 | public override void GetViolations(PolicyAnalysisContext context) 12 | { 13 | foreach (var repo in context.Org.Repos) 14 | { 15 | if (repo.IsFork || repo.IsTemporaryForkForSecurityAdvisory()) 16 | continue; 17 | 18 | if (repo.IsArchivedOrSoftArchived()) 19 | continue; 20 | 21 | if (!repo.IsOwnedByMicrosoft()) 22 | continue; 23 | 24 | // Let's ignore uninitialized repos 25 | if (repo.DefaultBranch is null) 26 | continue; 27 | 28 | if (!repo.DefaultBranch.Rules.Any()) 29 | { 30 | context.ReportViolation( 31 | Descriptor, 32 | $"The default branch '{repo.DefaultBranch.Name}' in '{repo.Name}' has no branch protection", 33 | $@" 34 | The default branch {repo.DefaultBranch.Markdown()} in repo {repo.Markdown()} should have branch protection rules, such as preventing force pushes and requiring PRs. 35 | ", 36 | repo: repo, 37 | branch: repo.DefaultBranch 38 | ); 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/Rules/PR20_ReleaseBranchesShouldBeProtected.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.DotnetOrg.Policies.Rules; 2 | 3 | internal sealed class PR20_ReleaseBranchesShouldBeProtected : PolicyRule 4 | { 5 | public override PolicyDescriptor Descriptor { get; } = new PolicyDescriptor( 6 | "PR20", 7 | "Release branches should have branch protection", 8 | PolicySeverity.Warning 9 | ); 10 | 11 | public override void GetViolations(PolicyAnalysisContext context) 12 | { 13 | foreach (var repo in context.Org.Repos) 14 | { 15 | if (repo.IsFork || repo.IsTemporaryForkForSecurityAdvisory()) 16 | continue; 17 | 18 | if (repo.IsArchivedOrSoftArchived()) 19 | continue; 20 | 21 | if (!repo.IsOwnedByMicrosoft()) 22 | continue; 23 | 24 | var unprotectedReleaseBranches = repo.Branches.Where(b => b.Name.StartsWith("release/", StringComparison.OrdinalIgnoreCase)) 25 | .Where(b => !b.Rules.Any()); 26 | 27 | foreach (var branch in unprotectedReleaseBranches) 28 | { 29 | context.ReportViolation( 30 | Descriptor, 31 | $"The release branch '{branch.Name}' in '{repo.Name}' has no branch protection", 32 | $@" 33 | The branch {branch.Markdown()} in repo {repo.Markdown()} appears to be a release branch and should have branch protection rules, such as preventing force pushes and requiring PRs. 34 | ", 35 | repo: repo, 36 | branch: branch 37 | ); 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/Rules/PR21_MicrosoftOwnedReposShouldNotUseSecrets.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | namespace Microsoft.DotnetOrg.Policies.Rules; 4 | 5 | internal sealed class PR21_MicrosoftOwnedReposShouldNotUseSecrets : PolicyRule 6 | { 7 | public override PolicyDescriptor Descriptor { get; } = new PolicyDescriptor( 8 | "PR21", 9 | "Microsoft-owned repo should not use secrets", 10 | PolicySeverity.Hidden 11 | ); 12 | 13 | public override void GetViolations(PolicyAnalysisContext context) 14 | { 15 | foreach (var repo in context.Org.Repos) 16 | { 17 | if (repo.IsArchivedOrSoftArchived()) 18 | continue; 19 | 20 | if (!repo.IsOwnedByMicrosoft()) 21 | continue; 22 | 23 | var secrets = repo.OrgSecrets.Cast() 24 | .Concat(repo.Secrets) 25 | .Concat(repo.Environments.SelectMany(e => e.Secrets)) 26 | .Distinct(); 27 | 28 | foreach (var secret in secrets) 29 | { 30 | context.ReportViolation( 31 | Descriptor, 32 | $"Repo '{repo.Name}' should not use secret '{secret.Name}'", 33 | $@" 34 | The repo {repo.Markdown()} is a Microsoft-owned repo and thus shouldn't use secrets on GitHub. Rather, secret state should be kept in the internal Azure DevOps fork. 35 | ", 36 | repo: repo, 37 | secret: secret 38 | ); 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/Rules/PR22_MicrosoftOwnedReposShouldDisableGitHubActionsWhenNotUsed.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | namespace Microsoft.DotnetOrg.Policies.Rules; 4 | 5 | internal sealed class PR22_MicrosoftOwnedReposShouldDisableGitHubActionsWhenNotUsed : PolicyRule 6 | { 7 | public override PolicyDescriptor Descriptor { get; } = new PolicyDescriptor( 8 | "PR22", 9 | "Microsoft-owned repo should disable GitHub Actions when it's not used", 10 | PolicySeverity.Error 11 | ); 12 | 13 | public override void GetViolations(PolicyAnalysisContext context) 14 | { 15 | foreach (var repo in context.Org.Repos) 16 | { 17 | if (!repo.IsOwnedByMicrosoft()) 18 | continue; 19 | 20 | if (repo.Workflows.Any()) 21 | continue; 22 | 23 | if (repo.ActionPermissions.AllowedActions == CachedRepoAllowedActions.Disabled) 24 | continue; 25 | 26 | context.ReportViolation( 27 | Descriptor, 28 | $"Repo '{repo.Name}' should disable GitHub Actions", 29 | $@" 30 | The repo {repo.Markdown()} doesn't have any workflows and thus should disable GitHub actions. 31 | ", 32 | repo: repo 33 | ); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/Rules/PR23_MicrosoftOwnedReposShouldRestrictGitHubActions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | namespace Microsoft.DotnetOrg.Policies.Rules; 4 | 5 | internal sealed class PR23_MicrosoftOwnedReposShouldRestrictGitHubActions : PolicyRule 6 | { 7 | public override PolicyDescriptor Descriptor { get; } = new PolicyDescriptor( 8 | "PR23", 9 | "Microsoft-owned repos should restrict GitHub Actions", 10 | PolicySeverity.Error 11 | ); 12 | 13 | public override void GetViolations(PolicyAnalysisContext context) 14 | { 15 | foreach (var repo in context.Org.Repos) 16 | { 17 | if (!repo.IsOwnedByMicrosoft()) 18 | continue; 19 | 20 | if (!repo.Workflows.Any()) 21 | continue; 22 | 23 | if (repo.ActionPermissions.AllowedActions == CachedRepoAllowedActions.Disabled || 24 | repo.ActionPermissions.AllowedActions == CachedRepoAllowedActions.LocalOnly || 25 | repo.ActionPermissions.AllowedActions == CachedRepoAllowedActions.Selected) 26 | continue; 27 | 28 | context.ReportViolation( 29 | Descriptor, 30 | $"Repo '{repo.Name}' should restrict GitHub Actions", 31 | $@" 32 | The repo {repo.Markdown()} shouldn't allow all GitHub Actions but restrict which actions can be used by either selecting **Local Only** or by [specifying a list of patterns](https://docs.github.com/en/github/administering-a-repository/managing-repository-settings/disabling-or-limiting-github-actions-for-a-repository#allowing-specific-actions-to-run) that describe which actions are allowed. 33 | ", 34 | repo: repo 35 | ); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/Rules/PR24_MicrosoftOwnedReposShouldNotUseExternalContributorsForRead.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | namespace Microsoft.DotnetOrg.Policies.Rules; 4 | 5 | internal sealed class PR24_MicrosoftOwnedReposShouldNotUseExternalContributorsForRead : PolicyRule 6 | { 7 | public override PolicyDescriptor Descriptor { get; } = new PolicyDescriptor( 8 | "PR24", 9 | "Microsoft-owned repos should not give read access to external contributors", 10 | PolicySeverity.Warning 11 | ); 12 | 13 | public override void GetViolations(PolicyAnalysisContext context) 14 | { 15 | var team = context.Org.Teams.SingleOrDefault(t => string.Equals(t.Name, "external-ci-access")); 16 | if (team is null) 17 | return; 18 | 19 | foreach (var repo in context.Org.Repos) 20 | { 21 | if (!repo.IsOwnedByMicrosoft()) 22 | continue; 23 | 24 | var collaborators = repo.Users.Where(u => !u.User.IsBot() && 25 | u.Permission == CachedPermission.Read && 26 | u.Describe().IsCollaborator); 27 | foreach (var collaborator in collaborators) 28 | { 29 | context.ReportViolation( 30 | Descriptor, 31 | $"Repo '{repo.Name}' should not give explicit read access to user '{collaborator.User.Login}'", 32 | $@" 33 | For read access, add the user {collaborator.User.Markdown()} to {team.Markdown()} and grant that team read access to {repo.Markdown()}. 34 | ", 35 | repo: repo, 36 | user: collaborator.User 37 | ); 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/Microsoft.DotnetOrg.Policies/Rules/PR25_MicrosoftOwnedPrivateReposNotShouldGrantAccessToNonMicrosoftUsers.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.DotnetOrg.Policies.Rules; 2 | 3 | internal sealed class PR25_MicrosoftOwnedPrivateReposNotShouldGrantAccessViaNonMicrosoftTeams : PolicyRule 4 | { 5 | public override PolicyDescriptor Descriptor { get; } = new PolicyDescriptor( 6 | "PR25", 7 | "Microsoft-owned private repos should not grant access via any non-Microsoft owned teams", 8 | PolicySeverity.Error 9 | ); 10 | 11 | public override void GetViolations(PolicyAnalysisContext context) 12 | { 13 | foreach (var repo in context.Org.Repos) 14 | { 15 | if (!repo.IsOwnedByMicrosoft()) 16 | continue; 17 | 18 | if (!repo.IsPrivate) 19 | continue; 20 | 21 | var nonMicrosoftTeamAccess = repo.Teams.Where(t => !t.Team.IsOwnedByMicrosoft() && 22 | !t.Team.IsInfrastructure()); 23 | 24 | foreach (var access in nonMicrosoftTeamAccess) 25 | { 26 | var team = access.Team; 27 | var permission = access.Permission; 28 | 29 | context.ReportViolation( 30 | Descriptor, 31 | $"Microsoft owned private repo '{repo.Name}' should not grant access via non-Microsoft owned team '{team.GetFullSlug()}'", 32 | $@" 33 | The repo {repo.Markdown()} gives {permission.Markdown()} access to a non-Microsoft owned team {team.Markdown()}. Either remove the team from the repo or make the team owned by Microsoft. Alternatively, if giving access is intentional, provide a justification below and apply an explicit policy override. 34 | ", 35 | repo: repo, 36 | team: team 37 | ); 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/policop/CacheManager.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Microsoft.DotnetOrg.GitHubCaching; 3 | 4 | namespace Microsoft.DotnetOrg.PolicyCop; 5 | 6 | internal static class CacheManager 7 | { 8 | public static IEnumerable GetCachedOrgNames() 9 | { 10 | return GetOrgCaches().Select(fi => Path.GetFileNameWithoutExtension(fi.Name)); 11 | } 12 | 13 | public static string GetOrgCacheDirectory() 14 | { 15 | var exePath = Environment.GetCommandLineArgs()[0]; 16 | var fileInfo = FileVersionInfo.GetVersionInfo(exePath)!; 17 | var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); 18 | var companyName = fileInfo.CompanyName ?? string.Empty; 19 | var productName = fileInfo.ProductName ?? string.Empty; 20 | var cachedDirectory = Path.Combine(localAppData, companyName, productName, "Cache"); 21 | return cachedDirectory; 22 | } 23 | 24 | public static FileInfo GetOrgCache(string orgName) 25 | { 26 | var cachedDirectory = GetOrgCacheDirectory(); 27 | var path = Path.Combine(cachedDirectory, $"{orgName}.json"); 28 | return new FileInfo(path); 29 | } 30 | 31 | public static IEnumerable GetOrgCaches() 32 | { 33 | var orgCacheDirectory = Path.GetDirectoryName(GetOrgCache("dummy").FullName)!; 34 | if (!Directory.Exists(orgCacheDirectory)) 35 | return Array.Empty(); 36 | 37 | var cachedOrgs = Directory.EnumerateFiles(orgCacheDirectory, "*.json"); 38 | return cachedOrgs.Select(o => new FileInfo(o)); 39 | } 40 | 41 | public static async Task LoadOrgAsync(string orgName) 42 | { 43 | var location = GetOrgCache(orgName); 44 | var cachedOrg = await CachedOrg.LoadAsync(location.FullName); 45 | 46 | var cacheIsValid = cachedOrg is not null && 47 | cachedOrg.Name == orgName && 48 | cachedOrg.Version == CachedOrg.CurrentVersion; 49 | 50 | return cacheIsValid ? cachedOrg : null; 51 | } 52 | 53 | public static Task StoreOrgAsync(CachedOrg result) 54 | { 55 | var location = GetOrgCache(result.Name); 56 | return result.SaveAsync(location.FullName); 57 | } 58 | } -------------------------------------------------------------------------------- /src/policop/Commands/AssignTeamCommand.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | using Mono.Options; 4 | 5 | using Octokit; 6 | 7 | namespace Microsoft.DotnetOrg.PolicyCop.Commands; 8 | 9 | internal sealed class AssignTeamCommand : ToolCommand 10 | { 11 | private string? _orgName; 12 | private string? _repoName; 13 | private string? _teamName; 14 | private string? _permission; 15 | private bool _unassign; 16 | 17 | public override string Name => "assign-team"; 18 | 19 | public override string Description => "Assigns or unassigns a team"; 20 | 21 | public override void AddOptions(OptionSet options) 22 | { 23 | options.AddOrg(v => _orgName = v) 24 | .Add("r=", "Specifies the repo", v => _repoName = v) 25 | .Add("t=", "Specifies the team", v => _teamName = v) 26 | .Add("p=", "Sets the {permission} (default: read)", v => _permission = v) 27 | .Add("d", "Unassigns the team", v => _unassign = true); 28 | } 29 | 30 | public override async Task ExecuteAsync() 31 | { 32 | if (string.IsNullOrEmpty(_orgName)) 33 | { 34 | Console.Error.WriteLine($"error: --org must be specified"); 35 | return; 36 | } 37 | 38 | if (string.IsNullOrEmpty(_repoName)) 39 | { 40 | Console.Error.WriteLine($"error: -r must be specified"); 41 | return; 42 | } 43 | 44 | if (string.IsNullOrEmpty(_teamName)) 45 | { 46 | Console.Error.WriteLine($"error: -t must be specified"); 47 | return; 48 | } 49 | 50 | switch (_permission) 51 | { 52 | case null: 53 | _permission = "pull"; 54 | goto case "pull"; 55 | case "pull": 56 | case "push": 57 | case "admin": 58 | case "maintain": 59 | case "triage": 60 | break; 61 | default: 62 | Console.Error.WriteLine($"error: permission can be 'pull', 'push', 'admin', 'maintain', or 'triage' but not '{_permission}'"); 63 | return; 64 | } 65 | 66 | var client = await GitHubClientFactory.CreateAsync(); 67 | var teams = await client.Organization.Team.GetAll(_orgName); 68 | 69 | var team = teams.SingleOrDefault(t => string.Equals(t.Name, _teamName, StringComparison.OrdinalIgnoreCase) || 70 | string.Equals(t.Slug, _teamName, StringComparison.OrdinalIgnoreCase)); 71 | 72 | if (team is null) 73 | { 74 | Console.Error.WriteLine($"error: team '{_teamName}' doesn't exist"); 75 | return; 76 | } 77 | 78 | Repository? repo; 79 | try 80 | { 81 | repo = await client.Repository.Get(_orgName, _repoName); 82 | } 83 | catch (Exception) 84 | { 85 | repo = null; 86 | } 87 | 88 | if (repo is null) 89 | { 90 | Console.Error.WriteLine($"error: repo '{_orgName}/{_repoName}' doesn't exist"); 91 | return; 92 | } 93 | 94 | if (_unassign) 95 | { 96 | await client.Organization.Team.RemoveRepository(team.Id, _orgName, _repoName); 97 | Console.Out.WriteLine($"Removed team '{_teamName}' from repo '{_repoName}'"); 98 | } 99 | else 100 | { 101 | await client.Connection.Put(new Uri($"/orgs/{_orgName}/teams/{team.Slug}/repos/{_orgName}/{_repoName}", UriKind.Relative), 102 | new { permission = _permission }); 103 | Console.Out.WriteLine($"Added team '{_teamName}' to repo '{_repoName}'"); 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /src/policop/Commands/AssignUserCommand.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | using Mono.Options; 4 | 5 | using Octokit; 6 | 7 | namespace Microsoft.DotnetOrg.PolicyCop.Commands; 8 | 9 | internal sealed class AssignUserCommand : ToolCommand 10 | { 11 | private string? _orgName; 12 | private string? _userName; 13 | private string? _repoName; 14 | private string? _teamName; 15 | private string? _permission; 16 | private bool _unassign; 17 | 18 | public override string Name => "assign-user"; 19 | 20 | public override string Description => "Assigns or unassigns a user to a repo or team"; 21 | 22 | public override void AddOptions(OptionSet options) 23 | { 24 | options.AddOrg(v => _orgName = v) 25 | .Add("u=", "Specifies the user", v => _userName = v) 26 | .Add("r=", "Specifies the repo", v => _repoName = v) 27 | .Add("t=", "Specifies the team", v => _teamName = v) 28 | .Add("p=", "Sets the {permission} (default: read)", v => _permission = v) 29 | .Add("d", "Unassigns the user or team", v => _unassign = true); 30 | } 31 | 32 | public override async Task ExecuteAsync() 33 | { 34 | if (string.IsNullOrEmpty(_orgName)) 35 | { 36 | Console.Error.WriteLine($"error: --org must be specified"); 37 | return; 38 | } 39 | 40 | if (string.IsNullOrEmpty(_userName)) 41 | { 42 | Console.Error.WriteLine($"error: -u must be specified"); 43 | return; 44 | } 45 | 46 | if (!string.IsNullOrEmpty(_repoName) && !string.IsNullOrEmpty(_teamName)) 47 | { 48 | Console.Error.WriteLine($"error: cannot specify both -r and -t"); 49 | return; 50 | } 51 | 52 | if (string.IsNullOrEmpty(_repoName) && string.IsNullOrEmpty(_teamName)) 53 | { 54 | Console.Error.WriteLine($"error: either -r or -t must be specified"); 55 | return; 56 | } 57 | 58 | string permission; 59 | 60 | switch (_permission) 61 | { 62 | case null: 63 | case "read": 64 | permission = "pull"; 65 | break; 66 | case "write": 67 | permission = "push"; 68 | break; 69 | case "admin": 70 | permission = "admin"; 71 | break; 72 | default: 73 | Console.Error.WriteLine($"error: permission can be 'read', 'write', or 'admin' but not '{_permission}'"); 74 | return; 75 | } 76 | 77 | var client = await GitHubClientFactory.CreateAsync(); 78 | 79 | User user; 80 | 81 | try 82 | { 83 | user = await client.User.Get(_userName); 84 | } 85 | catch (Exception) 86 | { 87 | Console.Error.WriteLine($"error: user '{_userName}' doesn't exist"); 88 | return; 89 | } 90 | 91 | if (!string.IsNullOrEmpty(_teamName)) 92 | { 93 | var teams = await client.Organization.Team.GetAll(_orgName); 94 | 95 | var team = teams.SingleOrDefault(t => string.Equals(t.Name, _teamName, StringComparison.OrdinalIgnoreCase) || 96 | string.Equals(t.Slug, _teamName, StringComparison.OrdinalIgnoreCase)); 97 | 98 | if (team is null) 99 | { 100 | Console.Error.WriteLine($"error: team '{_teamName}' doesn't exist"); 101 | return; 102 | } 103 | 104 | if (_unassign) 105 | { 106 | await client.Organization.Team.RemoveMembership(team.Id, _userName); 107 | Console.Out.WriteLine($"Removed user '{_userName}' from team '{_teamName}'"); 108 | } 109 | else 110 | { 111 | await client.Organization.Team.AddOrEditMembership(team.Id, _userName, new UpdateTeamMembership(TeamRole.Member)); 112 | Console.Out.WriteLine($"Added user '{_userName}' to team '{_teamName}'"); 113 | } 114 | } 115 | else if (!string.IsNullOrEmpty(_repoName)) 116 | { 117 | Repository? repo; 118 | try 119 | { 120 | repo = await client.Repository.Get(_orgName, _repoName); 121 | } 122 | catch (Exception) 123 | { 124 | repo = null; 125 | } 126 | 127 | if (repo is null) 128 | { 129 | Console.Error.WriteLine($"error: repo '{_orgName}/{_repoName}' doesn't exist"); 130 | return; 131 | } 132 | 133 | if (_unassign) 134 | { 135 | await client.Repository.Collaborator.Delete(_orgName, _repoName, _userName); 136 | Console.Out.WriteLine($"Removed user '{_userName}' from repo '{_repoName}'"); 137 | } 138 | else 139 | { 140 | await client.Repository.Collaborator.Add(_orgName, _repoName, _userName, new CollaboratorRequest(permission)); 141 | Console.Out.WriteLine($"Added user '{_userName}' to repo '{_repoName}'"); 142 | } 143 | } 144 | } 145 | } -------------------------------------------------------------------------------- /src/policop/Commands/AuditActionsCommand.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Csv; 2 | 3 | using Mono.Options; 4 | 5 | namespace Microsoft.DotnetOrg.PolicyCop.Commands; 6 | 7 | internal sealed class AuditActionsCommand : ToolCommand 8 | { 9 | private string? _orgName; 10 | private string? _outputFileName; 11 | private bool _viewInExcel; 12 | 13 | public override string Name => "audit-actions"; 14 | 15 | public override string Description => "Creates a log that shows which actions are being used by which repo and workflow"; 16 | 17 | public override void AddOptions(OptionSet options) 18 | { 19 | options.AddOrg(v => _orgName = v) 20 | .Add("o|output=", "The {path} where the output .csv file should be written to.", v => _outputFileName = v) 21 | .Add("excel", "Shows the results in Excel", v => _viewInExcel = true); 22 | } 23 | 24 | public override async Task ExecuteAsync() 25 | { 26 | if (_viewInExcel && !ExcelExtensions.IsExcelInstalled()) 27 | { 28 | Console.Error.WriteLine("error: --excel is only valid if Excel is installed."); 29 | return; 30 | } 31 | 32 | var orgTasks = GetOrgNames().Select(n => (Name: n, Task: CacheManager.LoadOrgAsync(n))); 33 | 34 | foreach (var (orgName, orgTask) in orgTasks) 35 | { 36 | var org = await orgTask; 37 | if (org is null) 38 | { 39 | Console.Error.WriteLine($"error: org '{_orgName}' not cached yet. Run cache-build or cache-org first."); 40 | return; 41 | } 42 | } 43 | 44 | var orgs = orgTasks.Select(t => t.Task.Result!); 45 | 46 | var csvDocument = new CsvDocument("org", "repo", "workflow", "action-org", "action-repo", "action-version"); 47 | using (var writer = csvDocument.Append()) 48 | { 49 | foreach (var org in orgs) 50 | { 51 | foreach (var repo in org.Repos) 52 | { 53 | foreach (var workflow in repo.Workflows) 54 | { 55 | foreach (var (actionOrg, actionRepo, actionVersion) in ParseActions(workflow.Contents)) 56 | { 57 | writer.Write(org.Name); 58 | writer.Write(repo.Name); 59 | writer.Write(workflow.Name); 60 | writer.Write(actionOrg); 61 | writer.Write(actionRepo); 62 | writer.Write(actionVersion); 63 | writer.WriteLine(); 64 | } 65 | } 66 | } 67 | } 68 | } 69 | 70 | if (_outputFileName is not null) 71 | csvDocument.Save(_outputFileName); 72 | 73 | if (_viewInExcel) 74 | csvDocument.ViewInExcel(); 75 | else 76 | csvDocument.PrintToConsole(); 77 | } 78 | 79 | private IEnumerable GetOrgNames() 80 | { 81 | if (string.IsNullOrEmpty(_orgName) || _orgName == "*") 82 | return CacheManager.GetCachedOrgNames(); 83 | 84 | return new[] { _orgName! }; 85 | } 86 | 87 | private static IEnumerable<(string ActionOrg, string ActionRepo, string ActionVersion)> ParseActions(string contents) 88 | { 89 | using var stringReader = new StringReader(contents); 90 | 91 | while (stringReader.ReadLine() is string line) 92 | { 93 | if (line.Contains("uses", StringComparison.OrdinalIgnoreCase)) 94 | { 95 | var indexOfColon = line.IndexOf(":"); 96 | if (indexOfColon >= 0) 97 | { 98 | var reference = line.Substring(indexOfColon + 1).Trim(); 99 | var indexOfAt = reference.IndexOf("@"); 100 | if (indexOfAt >= 0) 101 | { 102 | var orgAndRepo = reference.Substring(0, indexOfAt).Trim(); 103 | var version = reference.Substring(indexOfAt + 1).Trim(); 104 | 105 | var indexOfSlash = orgAndRepo.IndexOf("/"); 106 | if (indexOfSlash >= 0) 107 | { 108 | var org = orgAndRepo.Substring(0, indexOfSlash).Trim(); 109 | var repo = orgAndRepo.Substring(indexOfSlash + 1).Trim(); 110 | yield return (org, repo, version); 111 | } 112 | } 113 | } 114 | } 115 | } 116 | } 117 | } -------------------------------------------------------------------------------- /src/policop/Commands/AuditCommand.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Csv; 2 | 3 | using Mono.Options; 4 | 5 | namespace Microsoft.DotnetOrg.PolicyCop.Commands; 6 | 7 | internal sealed class AuditCommand : ToolCommand 8 | { 9 | private string? _orgName; 10 | private string? _outputFileName; 11 | private bool _viewInExcel; 12 | 13 | public override string Name => "audit"; 14 | 15 | public override string Description => "Produces a permission report for users and teams"; 16 | 17 | public override void AddOptions(OptionSet options) 18 | { 19 | options.AddOrg(v => _orgName = v) 20 | .Add("o|output=", "The {path} where the output .csv file should be written to.", v => _outputFileName = v) 21 | .Add("excel", "Shows the results in Excel", v => _viewInExcel = true); 22 | } 23 | 24 | public override async Task ExecuteAsync() 25 | { 26 | if (_orgName is null) 27 | { 28 | Console.Error.WriteLine($"error: --org must be specified"); 29 | return; 30 | } 31 | 32 | if (_viewInExcel && !ExcelExtensions.IsExcelInstalled()) 33 | { 34 | Console.Error.WriteLine("error: --excel is only valid if Excel is installed."); 35 | return; 36 | } 37 | 38 | var org = await CacheManager.LoadOrgAsync(_orgName); 39 | 40 | if (org is null) 41 | { 42 | Console.Error.WriteLine($"error: org '{_orgName}' not cached yet. Run cache-build or cache-org first."); 43 | return; 44 | } 45 | 46 | var csvDocument = new CsvDocument("repo", "repo-state", "repo-last-pushed", "principal-kind", "principal", "permission", "via-team"); 47 | using (var writer = csvDocument.Append()) 48 | { 49 | foreach (var repo in org.Repos) 50 | { 51 | var publicPrivate = repo.IsPrivate ? "private" : "public"; 52 | var lastPush = repo.LastPush.ToLocalTime().DateTime.ToString(); 53 | 54 | foreach (var teamAccess in repo.Teams) 55 | { 56 | var permissions = teamAccess.Permission.ToString().ToLower(); 57 | var teamName = teamAccess.Team.Name; 58 | var teamUrl = teamAccess.Team.Url; 59 | 60 | writer.WriteHyperlink(repo.Url, repo.Name, _viewInExcel); 61 | writer.Write(publicPrivate); 62 | writer.Write(lastPush); 63 | writer.Write("team"); 64 | writer.WriteHyperlink(teamUrl, teamName, _viewInExcel); 65 | writer.Write(permissions); 66 | writer.Write(teamName); 67 | writer.WriteLine(); 68 | } 69 | 70 | foreach (var userAccess in repo.EffectiveUsers) 71 | { 72 | var via = userAccess.Describe().ToString(); 73 | var userUrl = userAccess.User.Url; 74 | var permissions = userAccess.Permission.ToString().ToLower(); 75 | 76 | writer.WriteHyperlink(repo.Url, repo.Name, _viewInExcel); 77 | writer.Write(publicPrivate); 78 | writer.Write(lastPush); 79 | writer.Write("user"); 80 | writer.WriteHyperlink(userUrl, userAccess.UserLogin, _viewInExcel); 81 | writer.Write(permissions); 82 | writer.Write(via); 83 | writer.WriteLine(); 84 | } 85 | } 86 | } 87 | 88 | if (_outputFileName is not null) 89 | csvDocument.Save(_outputFileName); 90 | 91 | if (_viewInExcel) 92 | csvDocument.ViewInExcel(); 93 | else 94 | csvDocument.PrintToConsole(); 95 | } 96 | } -------------------------------------------------------------------------------- /src/policop/Commands/AuditLogCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using Microsoft.Csv; 3 | using Microsoft.DotnetOrg.GitHubCaching; 4 | 5 | using Mono.Options; 6 | 7 | using Newtonsoft.Json.Linq; 8 | 9 | namespace Microsoft.DotnetOrg.PolicyCop.Commands; 10 | 11 | internal sealed class AuditLogCommand : ToolCommand 12 | { 13 | private string? _orgName; 14 | private bool _viewInExcel; 15 | private string? _outputFileName; 16 | private readonly List _phrases = new(); 17 | 18 | public override string Name => "audit-log"; 19 | 20 | public override string Description => "Searches the GitHub Audit Log"; 21 | 22 | public override void AddOptions(OptionSet options) 23 | { 24 | options.AddOrg(v => _orgName = v) 25 | .Add("excel", "Shows the results in Excel", v => _viewInExcel = true) 26 | .Add("o|output=", "The {path} where the output .csv file should be written to.", v => _outputFileName = v) 27 | .Add("<>", v => _phrases.Add(v)); 28 | } 29 | 30 | public override async Task ExecuteAsync() 31 | { 32 | if (string.IsNullOrEmpty(_orgName)) 33 | { 34 | Console.Error.WriteLine($"error: --org must be specified"); 35 | return; 36 | } 37 | 38 | if (_viewInExcel && !ExcelExtensions.IsExcelInstalled()) 39 | { 40 | Console.Error.WriteLine("error: --excel is only valid if Excel is installed."); 41 | return; 42 | } 43 | 44 | var client = await GitHubClientFactory.CreateAsync(); 45 | var phrase = string.Join(" ", _phrases); 46 | var response = await client.Connection.GetRaw(new Uri($"/orgs/{_orgName}/audit-log", UriKind.Relative), new Dictionary() 47 | { 48 | { "phrase", phrase}, 49 | { "include", "all" } 50 | }); 51 | 52 | var array = JArray.Parse((string)response.HttpResponse.Body); 53 | 54 | var keys = array.Cast() 55 | .SelectMany(o => o.Properties().Select(p => p.Name)) 56 | .Distinct() 57 | .ToArray(); 58 | 59 | var document = new CsvDocument(keys); 60 | using (var writer = document.Append()) 61 | { 62 | foreach (var o in array.Cast()) 63 | { 64 | foreach (var key in keys) 65 | { 66 | if (!o.TryGetValue(key, out var value)) 67 | { 68 | writer.Write(""); 69 | } 70 | else if (key == "@timestamp" || key == "created_at") 71 | { 72 | var timestamp = DateTimeOffset.FromUnixTimeMilliseconds((long)value); 73 | writer.Write(timestamp.ToString()); 74 | } 75 | else 76 | { 77 | writer.Write(Regex.Replace(value.ToString(), $"\\s*\\n\\s*", " ")); 78 | } 79 | } 80 | 81 | writer.WriteLine(); 82 | } 83 | } 84 | 85 | if (_outputFileName is not null) 86 | document.Save(_outputFileName); 87 | else if (_viewInExcel) 88 | document.ViewInExcel(); 89 | else 90 | document.PrintToConsole(); 91 | } 92 | } -------------------------------------------------------------------------------- /src/policop/Commands/BlockUserCommand.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | using Mono.Options; 4 | 5 | namespace Microsoft.DotnetOrg.PolicyCop.Commands; 6 | 7 | internal sealed class BlockUserCommand : ToolCommand 8 | { 9 | private string? _orgName; 10 | private string? _userName; 11 | private bool _unblock; 12 | 13 | public override string Name => "block-user"; 14 | 15 | public override string Description => "Blocks or unblocks a user across all our orgs"; 16 | 17 | public override void AddOptions(OptionSet options) 18 | { 19 | options.AddOrg(v => _orgName = v) 20 | .Add("u=", "Specifies the user", v => _userName = v) 21 | .Add("unblock", "Unblocks the user", v => _unblock = true); 22 | } 23 | 24 | public override async Task ExecuteAsync() 25 | { 26 | string[] orgs; 27 | 28 | if (_orgName is null || _orgName == "*") 29 | orgs = CacheManager.GetCachedOrgNames().ToArray(); 30 | else 31 | orgs = new[] { _orgName }; 32 | 33 | if (string.IsNullOrEmpty(_userName)) 34 | { 35 | Console.Error.WriteLine($"error: -u must be specified"); 36 | return; 37 | } 38 | 39 | var client = await GitHubClientFactory.CreateAsync(); 40 | 41 | foreach (var org in orgs) 42 | { 43 | try 44 | { 45 | if (_unblock) 46 | { 47 | await client.Connection.Delete(new Uri($"/orgs/{org}/blocks/{_userName}", UriKind.Relative)); 48 | Console.WriteLine($"Unblocked {_userName} in {org}."); 49 | } 50 | else 51 | { 52 | await client.Connection.Put(new Uri($"/orgs/{org}/blocks/{_userName}", UriKind.Relative)); 53 | Console.WriteLine($"Blocked {_userName} in {org}."); 54 | } 55 | } 56 | catch (Exception ex) 57 | { 58 | Console.Error.WriteLine($"error: can't ban {_userName} in {org}: {ex.Message}"); 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/policop/Commands/CacheBuildCommand.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Compression; 2 | using System.Net.Http.Headers; 3 | using Humanizer; 4 | using Microsoft.DotnetOrg.GitHubCaching; 5 | using Mono.Options; 6 | 7 | namespace Microsoft.DotnetOrg.PolicyCop.Commands; 8 | 9 | internal sealed class CacheBuildCommand : ToolCommand 10 | { 11 | private string? _orgName; 12 | private string? _buildId; 13 | 14 | public override string Name => "cache-build"; 15 | 16 | public override string Description => "Caches the org from the latest build"; 17 | 18 | public override void AddOptions(OptionSet options) 19 | { 20 | options.AddOrg(v => _orgName = v) 21 | .Add("build=", "The (optional) build {id} to use.", v => _buildId = v); 22 | } 23 | 24 | public override async Task ExecuteAsync() 25 | { 26 | var repos = new (string Org, string PolicyRepoOrg, string PolicyRepo)[] 27 | { 28 | ("aspnet", "aspnet", "org-policy-violations"), 29 | ("dotnet", "dotnet", "org-policy-violations"), 30 | ("nuget", "dotnet", "nuget-policy-violations"), 31 | ("mono", "dotnet", "mono-policy-violations"), 32 | }; 33 | 34 | var client = await GitHubClientFactory.CreateAsync(); 35 | 36 | foreach (var (org, policyRepoOrg, policyRepo) in repos) 37 | { 38 | try 39 | { 40 | var artifacts = await client.GetActionArtifacts(policyRepoOrg, policyRepo); 41 | var latest = artifacts.FirstOrDefault(); 42 | if (latest is null) 43 | { 44 | Console.WriteLine($"{org,-7} -- No results yet"); 45 | } 46 | else 47 | { 48 | var age = DateTimeOffset.UtcNow - latest.CreatedAt; 49 | Console.WriteLine($"{org,-7} -- Caching build from {age.Humanize()} ago..."); 50 | 51 | var token = client.Credentials.Password; 52 | 53 | var exeName = Path.GetFileNameWithoutExtension(Environment.ProcessPath)!; 54 | var productHeader = new ProductInfoHeaderValue(new ProductHeaderValue(exeName)); 55 | 56 | using var httpClient = new HttpClient(); 57 | httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Token", token); 58 | httpClient.DefaultRequestHeaders.UserAgent.Add(productHeader); 59 | httpClient.DefaultRequestHeaders.Add("Accept", "application/vnd.github+json"); 60 | httpClient.DefaultRequestHeaders.Add("X-GitHub-Api-Version", "2022-11-28"); 61 | 62 | await using var responseStream = await httpClient.GetStreamAsync(latest.ArchiveDownloadUrl); 63 | await using var stream = new MemoryStream(); 64 | await responseStream.CopyToAsync(stream); 65 | using var archive = new ZipArchive(stream, ZipArchiveMode.Read); 66 | var entry = archive.Entries.FirstOrDefault(e => string.Equals(Path.GetExtension(e.Name), ".json", StringComparison.OrdinalIgnoreCase)); 67 | if (entry is not null) 68 | { 69 | var localFileName = CacheManager.GetOrgCache(org).FullName; 70 | var directory = Path.GetDirectoryName(localFileName)!; 71 | Directory.CreateDirectory(directory); 72 | 73 | using var sourceStream = entry.Open(); 74 | using var targetStream = File.Create(localFileName); 75 | await sourceStream.CopyToAsync(targetStream); 76 | } 77 | } 78 | } 79 | catch (Exception ex) 80 | { 81 | Console.Error.WriteLine($"{org,-7} -- Can't cache results: {ex.Message}"); 82 | } 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /src/policop/Commands/CacheClearCommand.cs: -------------------------------------------------------------------------------- 1 | using Mono.Options; 2 | 3 | namespace Microsoft.DotnetOrg.PolicyCop.Commands; 4 | 5 | internal sealed class CacheClearCommand : ToolCommand 6 | { 7 | private bool _force; 8 | 9 | public override string Name => "cache-clear"; 10 | 11 | public override string Description => "Clears the cached orgs"; 12 | 13 | public override void AddOptions(OptionSet options) 14 | { 15 | options.Add("f", "Actually clears the cache", v => _force = true); 16 | } 17 | 18 | public override Task ExecuteAsync() 19 | { 20 | foreach (var file in CacheManager.GetOrgCaches()) 21 | { 22 | Console.WriteLine($"rm: {file}"); 23 | 24 | if (_force) 25 | file.Delete(); 26 | } 27 | 28 | if (!_force && CacheManager.GetOrgCaches().Any()) 29 | { 30 | Console.WriteLine("info: no files deleted"); 31 | Console.WriteLine("info: to actually delete files, specify -f"); 32 | } 33 | 34 | return Task.CompletedTask; 35 | } 36 | } -------------------------------------------------------------------------------- /src/policop/Commands/CacheExportCommand.cs: -------------------------------------------------------------------------------- 1 | using Mono.Options; 2 | 3 | namespace Microsoft.DotnetOrg.PolicyCop.Commands; 4 | 5 | internal sealed class CacheExportCommand : ToolCommand 6 | { 7 | private string? _outputDirectory; 8 | 9 | public override string Name => "cache-export"; 10 | 11 | public override string Description => "Exports the cached orgs to a directory"; 12 | 13 | public override void AddOptions(OptionSet options) 14 | { 15 | options.Add("o=", "The {path} to the directory where the cache data should be written to.", v => _outputDirectory = v); 16 | } 17 | 18 | public override Task ExecuteAsync() 19 | { 20 | if (_outputDirectory is null) 21 | { 22 | Console.Error.WriteLine($"error: -o must be specified"); 23 | return Task.CompletedTask; 24 | } 25 | 26 | _outputDirectory = Path.GetFullPath(_outputDirectory); 27 | 28 | var orgCaches = CacheManager.GetOrgCaches(); 29 | 30 | foreach (var orgCache in orgCaches) 31 | { 32 | var destinationPath = Path.Combine(_outputDirectory, orgCache.Name); 33 | 34 | if (orgCache.Exists) 35 | { 36 | var destinationDirectoryPath = Path.GetDirectoryName(destinationPath)!; 37 | Directory.CreateDirectory(destinationDirectoryPath); 38 | orgCache.CopyTo(destinationPath, true); 39 | } 40 | } 41 | 42 | return Task.CompletedTask; 43 | } 44 | } -------------------------------------------------------------------------------- /src/policop/Commands/CacheInfoCommand.cs: -------------------------------------------------------------------------------- 1 | using Humanizer.Bytes; 2 | using Microsoft.Csv; 3 | 4 | using Mono.Options; 5 | 6 | namespace Microsoft.DotnetOrg.PolicyCop.Commands; 7 | 8 | internal sealed class CacheInfoCommand : ToolCommand 9 | { 10 | public override string Name => "cache-info"; 11 | 12 | public override string Description => "Displays information about the cached orgs"; 13 | 14 | public override void AddOptions(OptionSet options) 15 | { 16 | } 17 | 18 | public override Task ExecuteAsync() 19 | { 20 | var orgCaches = CacheManager.GetOrgCaches().ToArray(); 21 | if (orgCaches.Length == 0) 22 | { 23 | var orgCacheDirectory = CacheManager.GetOrgCacheDirectory(); 24 | Console.WriteLine($"No orgs cached in {orgCacheDirectory}"); 25 | return Task.CompletedTask; 26 | } 27 | 28 | var document = new CsvDocument("org", "date", "time", "size", "path"); 29 | 30 | using (var writer = document.Append()) 31 | { 32 | foreach (var orgCache in orgCaches) 33 | { 34 | writer.Write(Path.GetFileNameWithoutExtension(orgCache.Name)); 35 | writer.Write(orgCache.LastWriteTime.ToShortDateString()); 36 | writer.Write(orgCache.LastWriteTime.ToShortTimeString()); 37 | writer.Write(new ByteSize(orgCache.Length).ToString("N0")); 38 | writer.Write(orgCache.FullName); 39 | writer.WriteLine(); 40 | } 41 | } 42 | 43 | document.PrintToConsole(); 44 | 45 | return Task.CompletedTask; 46 | } 47 | } -------------------------------------------------------------------------------- /src/policop/Commands/CacheOrgCommand.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.DotnetOrg.GitHubCaching; 2 | using Microsoft.DotnetOrg.Ospo; 3 | 4 | using Mono.Options; 5 | 6 | namespace Microsoft.DotnetOrg.PolicyCop.Commands; 7 | 8 | internal sealed class CacheOrgCommand : ToolCommand 9 | { 10 | private string? _orgName; 11 | private bool _includeLinks; 12 | 13 | public override string Name => "cache-org"; 14 | 15 | public override string Description => "Downloads the organization data from GitHub"; 16 | 17 | public override void AddOptions(OptionSet options) 18 | { 19 | options.AddOrg(v => _orgName = v) 20 | .Add("with-ms-links", "Include linking information to Microsoft users", v => _includeLinks = true); 21 | } 22 | 23 | public override async Task ExecuteAsync() 24 | { 25 | var orgNames = !string.IsNullOrEmpty(_orgName) 26 | ? new[] { _orgName } 27 | : CacheManager.GetCachedOrgNames().ToArray(); 28 | 29 | var client = await GitHubClientFactory.CreateAsync(); 30 | var connection = await GitHubClientFactory.CreateGraphAsync(); 31 | var ospoClient = !_includeLinks ? null : await OspoClientFactory.CreateAsync(); 32 | 33 | foreach (var orgName in orgNames) 34 | { 35 | var result = await CachedOrg.LoadAsync(client, connection, orgName, Console.Out, ospoClient); 36 | if (result is not null) 37 | await CacheManager.StoreOrgAsync(result); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/policop/Commands/ChangeVisibilityCommand.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | using Mono.Options; 4 | 5 | using Octokit; 6 | 7 | namespace Microsoft.DotnetOrg.PolicyCop.Commands; 8 | 9 | internal sealed class ChangeVisibilityCommand : ToolCommand 10 | { 11 | private string? _orgName; 12 | private string? _repoName; 13 | private bool _makePrivate; 14 | private bool _makePublic; 15 | 16 | public override string Name => "change-visibility"; 17 | 18 | public override string Description => "Makes a repo public or private"; 19 | 20 | public override void AddOptions(OptionSet options) 21 | { 22 | options.AddOrg(v => _orgName = v) 23 | .Add("r=", "Specifies the {name} of the repo", v => _repoName = v) 24 | .Add("private", "Makes the repo private", v => _makePrivate = true) 25 | .Add("public", "Makes the repo public", v => _makePublic = true); 26 | } 27 | 28 | public override async Task ExecuteAsync() 29 | { 30 | if (string.IsNullOrEmpty(_orgName)) 31 | { 32 | Console.Error.WriteLine($"error: --org must be specified"); 33 | return; 34 | } 35 | 36 | if (string.IsNullOrEmpty(_repoName)) 37 | { 38 | Console.Error.WriteLine($"error: -r must be specified"); 39 | return; 40 | } 41 | 42 | if (!(_makePrivate ^ _makePublic)) 43 | { 44 | Console.Error.WriteLine($"error: must specify either --private or --public"); 45 | return; 46 | } 47 | 48 | var client = await GitHubClientFactory.CreateAsync(); 49 | 50 | var update = new RepositoryUpdate 51 | { 52 | Name = _repoName, 53 | Private = _makePrivate 54 | }; 55 | 56 | try 57 | { 58 | await client.Repository.Edit(_orgName, _repoName, update); 59 | } 60 | catch (ApiException ex) 61 | { 62 | Console.Error.WriteLine(ex.Message); 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /src/policop/Commands/IssuesCommand.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Csv; 2 | using Microsoft.DotnetOrg.GitHubCaching; 3 | 4 | using Mono.Options; 5 | 6 | using Octokit; 7 | 8 | namespace Microsoft.DotnetOrg.PolicyCop.Commands; 9 | 10 | internal sealed class IssuesCommand : ToolCommand 11 | { 12 | private string? _orgName; 13 | private string? _repoName; 14 | private readonly List _labels = new List(); 15 | private bool _viewInExcel; 16 | 17 | public override string Name => "issues"; 18 | 19 | public override string Description => "Shows the list of open issues"; 20 | 21 | public override void AddOptions(OptionSet options) 22 | { 23 | options.AddOrg(v => _orgName = v) 24 | .Add("r=", "Specifies the repo", v => _repoName = v) 25 | .Add("l=", "Specifies the label", v => _labels.Add(v)) 26 | .Add("excel", "Shows the results in Excel", v => _viewInExcel = true); 27 | } 28 | 29 | public override async Task ExecuteAsync() 30 | { 31 | if (string.IsNullOrEmpty(_orgName)) 32 | { 33 | Console.Error.WriteLine($"error: --org must be specified"); 34 | return; 35 | } 36 | 37 | if (string.IsNullOrEmpty(_repoName)) 38 | { 39 | Console.Error.WriteLine($"error: -r must be specified"); 40 | return; 41 | } 42 | 43 | if (_viewInExcel && !ExcelExtensions.IsExcelInstalled()) 44 | { 45 | Console.Error.WriteLine("error: --excel is only valid if Excel is installed."); 46 | return; 47 | } 48 | 49 | var client = await GitHubClientFactory.CreateAsync(); 50 | var request = new RepositoryIssueRequest(); 51 | request.Filter = IssueFilter.All; 52 | request.State = ItemStateFilter.Open; 53 | 54 | foreach (var label in _labels) 55 | request.Labels.Add(label); 56 | 57 | var issues = await client.Issue.GetAllForRepository(_orgName, _repoName, request); 58 | 59 | var document = new CsvDocument("Id", "Link", "Title", "Labels"); 60 | 61 | using (var writer = document.Append()) 62 | { 63 | foreach (var issue in issues) 64 | { 65 | var labelList = string.Join(", ", issue.Labels.Select(l => l.Name)); 66 | 67 | writer.Write($"{_orgName}/{_repoName}#{issue.Number}"); 68 | writer.Write($"{issue.HtmlUrl}"); 69 | writer.Write($"{issue.Title}"); 70 | writer.Write($"{labelList}"); 71 | writer.WriteLine(); 72 | } 73 | } 74 | 75 | if (_viewInExcel) 76 | document.ViewInExcel(); 77 | else 78 | document.PrintToConsole(); 79 | } 80 | } -------------------------------------------------------------------------------- /src/policop/Commands/ListColumnsCommand.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Csv; 2 | using Microsoft.DotnetOrg.PolicyCop.Reporting; 3 | 4 | using Mono.Options; 5 | 6 | namespace Microsoft.DotnetOrg.PolicyCop.Commands; 7 | 8 | internal sealed class ListColumnsCommand : ToolCommand 9 | { 10 | public override string Name => "list-columns"; 11 | 12 | public override string Description => "Shows the list of available columns"; 13 | 14 | public override void AddOptions(OptionSet options) 15 | { 16 | } 17 | 18 | public override Task ExecuteAsync() 19 | { 20 | var document = new CsvDocument("name", "description"); 21 | using (var writer = document.Append()) 22 | { 23 | foreach (var column in ReportColumn.All) 24 | { 25 | writer.Write(column.Name); 26 | writer.Write(column.Description); 27 | writer.WriteLine(); 28 | } 29 | } 30 | 31 | document.PrintToConsole(); 32 | return Task.CompletedTask; 33 | } 34 | } -------------------------------------------------------------------------------- /src/policop/Commands/ListRulesCommand.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Csv; 2 | using Microsoft.DotnetOrg.Policies; 3 | 4 | using Mono.Options; 5 | 6 | namespace Microsoft.DotnetOrg.PolicyCop.Commands; 7 | 8 | internal sealed class ListRulesCommand : ToolCommand 9 | { 10 | public override string Name => "list-rules"; 11 | 12 | public override string Description => "Shows the list of policy rules"; 13 | 14 | public override void AddOptions(OptionSet options) 15 | { 16 | } 17 | 18 | public override Task ExecuteAsync() 19 | { 20 | var document = new CsvDocument("id", "severity", "title"); 21 | using (var writer = document.Append()) 22 | { 23 | foreach (var rule in PolicyRunner.GetRules()) 24 | { 25 | writer.Write(rule.Descriptor.DiagnosticId); 26 | writer.Write(rule.Descriptor.Severity.ToString()); 27 | writer.Write(rule.Descriptor.Title); 28 | writer.WriteLine(); 29 | } 30 | } 31 | 32 | document.PrintToConsole(); 33 | return Task.CompletedTask; 34 | } 35 | } -------------------------------------------------------------------------------- /src/policop/Commands/ListTokensCommand.cs: -------------------------------------------------------------------------------- 1 | using Mono.Options; 2 | using System.Diagnostics; 3 | 4 | namespace Microsoft.DotnetOrg.PolicyCop.Commands; 5 | 6 | internal sealed class ListTokensCommand : ToolCommand 7 | { 8 | public override string Name => "list-tokens"; 9 | 10 | public override string Description => "Lists the stored access tokens."; 11 | 12 | public override void AddOptions(OptionSet options) 13 | { 14 | } 15 | 16 | public override Task ExecuteAsync() 17 | { 18 | var directory = GetTokenDirectory(); 19 | if (Directory.Exists(directory)) 20 | { 21 | foreach (var fileName in Directory.GetFiles(directory)) 22 | Console.WriteLine(fileName); 23 | } 24 | else 25 | { 26 | Console.WriteLine("Not tokens found in"); 27 | Console.WriteLine(directory); 28 | } 29 | 30 | return Task.CompletedTask; 31 | } 32 | 33 | private static string GetTokenDirectory() 34 | { 35 | var exePath = Environment.GetCommandLineArgs()[0]; 36 | var fileInfo = FileVersionInfo.GetVersionInfo(exePath)!; 37 | var companyName = fileInfo.CompanyName ?? string.Empty; 38 | var productName = fileInfo.ProductName ?? string.Empty; 39 | return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), companyName, productName); 40 | } 41 | } -------------------------------------------------------------------------------- /src/policop/Commands/MsLookupCommand.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Csv; 2 | using Microsoft.DotnetOrg.Ospo; 3 | 4 | using Mono.Options; 5 | 6 | namespace Microsoft.DotnetOrg.PolicyCop.Commands; 7 | 8 | internal sealed class MsLookupCommand : ToolCommand 9 | { 10 | private readonly List _terms = new List(); 11 | private bool _viewInExcel; 12 | 13 | public override string Name => "ms-lookup"; 14 | 15 | public override string Description => "Looks up a Microsoft user by alias, email, name or GitHub handle"; 16 | 17 | public override void AddOptions(OptionSet options) 18 | { 19 | options.Add("<>", v => _terms.Add(v)) 20 | .Add("excel", "Shows the results in Excel", v => _viewInExcel = true); 21 | } 22 | 23 | public override async Task ExecuteAsync() 24 | { 25 | if (_terms.Count == 0) 26 | { 27 | Console.Error.WriteLine("error: needs argument"); 28 | return; 29 | } 30 | 31 | var client = await OspoClientFactory.CreateAsync(); 32 | var linkSet = await client.GetAllAsync(); 33 | 34 | var document = new CsvDocument("GitHub", "Name", "Alias", "Email"); 35 | 36 | using (var writer = document.Append()) 37 | { 38 | foreach (var link in linkSet.Links) 39 | { 40 | var anyMatch = _terms.Any(t => IsMatch(link, t)); 41 | if (!anyMatch) 42 | continue; 43 | 44 | writer.Write($"{link.GitHubInfo.Login}"); 45 | writer.Write($"{link.MicrosoftInfo.PreferredName}"); 46 | writer.Write($"{link.MicrosoftInfo.Alias}"); 47 | writer.Write($"{link.MicrosoftInfo.EmailAddress}"); 48 | writer.WriteLine(); 49 | } 50 | } 51 | 52 | if (_viewInExcel) 53 | document.ViewInExcel(); 54 | else 55 | document.PrintToConsole(); 56 | } 57 | 58 | private static bool IsMatch(OspoLink link, string term) 59 | { 60 | var login = term.StartsWith("@") 61 | ? term.Substring(1) 62 | : term; 63 | 64 | var aliasText = term.Contains("@") 65 | ? term.Substring(0, term.IndexOf('@')) 66 | : term; 67 | 68 | return string.Equals(link.GitHubInfo?.Id.ToString(), term, StringComparison.OrdinalIgnoreCase) || 69 | string.Equals(link.GitHubInfo?.Login, login, StringComparison.OrdinalIgnoreCase) || 70 | string.Equals(link.MicrosoftInfo?.Alias, aliasText, StringComparison.OrdinalIgnoreCase) || 71 | string.Equals(link.MicrosoftInfo?.EmailAddress, term, StringComparison.OrdinalIgnoreCase) || 72 | string.Equals(link.MicrosoftInfo?.PreferredName, term, StringComparison.OrdinalIgnoreCase); 73 | } 74 | } -------------------------------------------------------------------------------- /src/policop/Commands/SetActionPermissionsCommand.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | using Mono.Options; 4 | 5 | namespace Microsoft.DotnetOrg.PolicyCop.Commands; 6 | 7 | internal sealed class SetActionPermissionsCommand : ToolCommand 8 | { 9 | private string? _orgName; 10 | private string? _repoName; 11 | private bool? _enable; 12 | 13 | public override string Name => "set-action-permissions"; 14 | 15 | public override string Description => "Enables or disables GitHub actions for a given repo."; 16 | 17 | public override void AddOptions(OptionSet options) 18 | { 19 | options.AddOrg(v => _orgName = v) 20 | .Add("r=", "Specifies the repo", v => _repoName = v) 21 | .Add("enable", "Enables actions", v => _enable = true) 22 | .Add("disable", "Disables actions", v => _enable = false); 23 | } 24 | 25 | public override async Task ExecuteAsync() 26 | { 27 | if (string.IsNullOrEmpty(_orgName)) 28 | { 29 | Console.Error.WriteLine($"error: --org must be specified"); 30 | return; 31 | } 32 | 33 | if (string.IsNullOrEmpty(_repoName)) 34 | { 35 | Console.Error.WriteLine($"error: -r must be specified"); 36 | return; 37 | } 38 | 39 | if (_enable is null) 40 | { 41 | Console.Error.WriteLine($"error: either --enable or --disable must be specified"); 42 | return; 43 | } 44 | 45 | var client = await GitHubClientFactory.CreateAsync(); 46 | 47 | try 48 | { 49 | await client.Connection.Put(new Uri($"/repos/{_orgName}/{_repoName}/actions/permissions", UriKind.Relative), new 50 | { 51 | enabled = _enable.Value 52 | }); 53 | } 54 | catch (Exception ex) 55 | { 56 | Console.Error.WriteLine($"error: {ex.Message}"); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/policop/Commands/SetParentTeamCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Microsoft.DotnetOrg.GitHubCaching; 3 | 4 | using Mono.Options; 5 | 6 | using Octokit; 7 | 8 | namespace Microsoft.DotnetOrg.PolicyCop.Commands; 9 | 10 | internal sealed class SetParentTeamCommand : ToolCommand 11 | { 12 | private string? _orgName; 13 | private string? _teamName; 14 | private string? _parentTeam; 15 | private bool _unassign; 16 | 17 | public override string Name => "set-parent-team"; 18 | 19 | public override string Description => "Sets the parent of a team"; 20 | 21 | public override void AddOptions(OptionSet options) 22 | { 23 | options.AddOrg(v => _orgName = v) 24 | .Add("t=", "Specifies the team", v => _teamName = v) 25 | .Add("p=", "Specifies the new team's parent", v => _parentTeam = v) 26 | .Add("d", "Unassigns the parent", v => _unassign = true); 27 | } 28 | 29 | public override async Task ExecuteAsync() 30 | { 31 | if (string.IsNullOrEmpty(_orgName)) 32 | { 33 | Console.Error.WriteLine($"error: --org must be specified"); 34 | return; 35 | } 36 | 37 | if (string.IsNullOrEmpty(_teamName)) 38 | { 39 | Console.Error.WriteLine($"error: -t must be specified"); 40 | return; 41 | } 42 | 43 | if (string.IsNullOrEmpty(_parentTeam) && !_unassign) 44 | { 45 | Console.Error.WriteLine($"error: either -p or -d must be specified"); 46 | return; 47 | } 48 | 49 | var client = await GitHubClientFactory.CreateAsync(); 50 | var teams = await client.Organization.Team.GetAll(_orgName); 51 | 52 | Team? FindTeam(string name) 53 | { 54 | var lastSlash = name.LastIndexOf('/'); 55 | if (lastSlash >= 0) 56 | name = name.Substring(lastSlash + 1); 57 | 58 | return teams.SingleOrDefault(t => string.Equals(t.Name, name, StringComparison.OrdinalIgnoreCase) || 59 | string.Equals(t.Slug, name, StringComparison.OrdinalIgnoreCase)); 60 | } 61 | 62 | var team = FindTeam(_teamName); 63 | var parentTeam = (Team?) null; 64 | 65 | if (team is null) 66 | { 67 | Console.Error.WriteLine($"error: team '{_teamName}' doesn't exist"); 68 | return; 69 | } 70 | 71 | if (_parentTeam is not null) 72 | { 73 | parentTeam = FindTeam(_parentTeam); 74 | if (parentTeam is null) 75 | { 76 | Console.Error.WriteLine($"error: parent team '{_parentTeam}' doesn't exist"); 77 | return; 78 | } 79 | } 80 | 81 | var updateTeam = new UpdateTeam(team.Name); 82 | 83 | if (_unassign) 84 | { 85 | updateTeam.ParentTeamId = null; 86 | await client.Organization.Team.Update(team.Id, updateTeam); 87 | Console.Out.WriteLine($"Cleared parent of '{team.Slug}'"); 88 | } 89 | else 90 | { 91 | Debug.Assert(parentTeam is not null); 92 | updateTeam.ParentTeamId = parentTeam.Id; 93 | await client.Organization.Team.Update(team.Id, updateTeam); 94 | Console.Out.WriteLine($"Set parent of '{team.Slug}' to '{parentTeam.Slug}'"); 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /src/policop/Commands/StatusCheckCommand.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | using Mono.Options; 4 | 5 | namespace Microsoft.DotnetOrg.PolicyCop.Commands; 6 | 7 | internal sealed class StatusCheckCommand : ToolCommand 8 | { 9 | private string? _orgName; 10 | private string? _repoName; 11 | private string _branchName = "*"; 12 | private string? _statusCheck; 13 | private bool _disable; 14 | 15 | public override string Name => "status-check"; 16 | 17 | public override string Description => "Shows or disables a given status check"; 18 | 19 | public override void AddOptions(OptionSet options) 20 | { 21 | options.AddOrg(v => _orgName = v) 22 | .Add("r=", "Specifies the {name} of the repo", v => _repoName = v) 23 | .Add("branch=", "Specifies the {name} of the branch", v => _branchName = v) 24 | .Add("check=", "Specifies the {name} of the status check", v => _statusCheck = v) 25 | .Add("disable", "Disables the given status check", v => _disable = true); 26 | } 27 | 28 | public override async Task ExecuteAsync() 29 | { 30 | if (string.IsNullOrEmpty(_orgName)) 31 | { 32 | Console.Error.WriteLine($"error: --org must be specified"); 33 | return; 34 | } 35 | 36 | if (string.IsNullOrEmpty(_repoName)) 37 | { 38 | Console.Error.WriteLine($"error: -r must be specified"); 39 | return; 40 | } 41 | 42 | if (string.IsNullOrEmpty(_statusCheck)) 43 | { 44 | Console.Error.WriteLine($"error: --check must be specified"); 45 | return; 46 | } 47 | 48 | var org = await CacheManager.LoadOrgAsync(_orgName); 49 | 50 | if (org is null) 51 | { 52 | Console.Error.WriteLine($"error: org '{_orgName}' not cached yet. Run cache-build or cache-org first."); 53 | return; 54 | } 55 | 56 | var client = await GitHubClientFactory.CreateAsync(); 57 | var matchingRepos = org.Repos.OrderBy(r => r.Name) 58 | .Where(r => _repoName == "*" || string.Equals(r.Name, _repoName, StringComparison.OrdinalIgnoreCase)) 59 | .ToArray(); 60 | 61 | if (matchingRepos.Length == 0) 62 | { 63 | Console.WriteLine($"warning: no repos found"); 64 | } 65 | else 66 | { 67 | foreach (var repo in matchingRepos) 68 | { 69 | Console.ForegroundColor = ConsoleColor.DarkGray; 70 | Console.WriteLine($"Checking {repo.FullName}..."); 71 | Console.ResetColor(); 72 | 73 | var branches = await client.Repository.Branch.GetAll(repo.Org.Name, repo.Name); 74 | var matchinBranches = branches.Where(b => b.Protected) 75 | .Where(b => _branchName == "*" || string.Equals(b.Name, _branchName, StringComparison.Ordinal)) 76 | .ToArray(); 77 | 78 | foreach (var branch in matchinBranches) 79 | { 80 | var protectionSettings = await client.Repository.Branch.GetBranchProtection(repo.Org.Name, repo.Name, branch.Name); 81 | 82 | if (protectionSettings?.RequiredStatusChecks is not null) 83 | { 84 | var contexts = protectionSettings.RequiredStatusChecks.Contexts.ToList(); 85 | var matchingContexts = contexts.Where(c => _statusCheck == "*" || string.Equals(c, _statusCheck, StringComparison.OrdinalIgnoreCase)) 86 | .ToArray(); 87 | 88 | foreach (var context in matchingContexts) 89 | { 90 | Console.Write($"{repo.FullName} @ {branch.Name} {context} "); 91 | 92 | if (!_disable) 93 | { 94 | Console.ForegroundColor = ConsoleColor.Green; 95 | Console.WriteLine($"[ENABLED]"); 96 | } 97 | else 98 | { 99 | Console.ForegroundColor = ConsoleColor.Red; 100 | Console.WriteLine($"[DISABLED]"); 101 | } 102 | 103 | Console.ResetColor(); 104 | } 105 | 106 | if (_disable && matchingContexts.Any()) 107 | { 108 | foreach (var context in matchingContexts) 109 | contexts.Remove(context); 110 | 111 | 112 | contexts.RemoveAll(c => string.Equals(c, _statusCheck, StringComparison.OrdinalIgnoreCase)); 113 | await client.Repository.Branch.UpdateRequiredStatusChecksContexts(repo.Org.Name, repo.Name, branch.Name, contexts); 114 | } 115 | } 116 | } 117 | } 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /src/policop/Commands/WhoAmICommand.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | using Mono.Options; 4 | 5 | namespace Microsoft.DotnetOrg.PolicyCop.Commands; 6 | 7 | internal sealed class WhoAmICommand : ToolCommand 8 | { 9 | public override string Name => "whoami"; 10 | 11 | public override string Description => "Displays the GitHub user account being used"; 12 | 13 | public override void AddOptions(OptionSet options) 14 | { 15 | } 16 | 17 | public override async Task ExecuteAsync() 18 | { 19 | var client = await GitHubClientFactory.CreateAsync(); 20 | var me = await client.User.Current(); 21 | Console.WriteLine(me.Login); 22 | } 23 | } -------------------------------------------------------------------------------- /src/policop/OptionSetExtensions.cs: -------------------------------------------------------------------------------- 1 | using Mono.Options; 2 | 3 | namespace Microsoft.DotnetOrg.PolicyCop; 4 | 5 | internal static class OptionSetExtensions 6 | { 7 | public static OptionSet AddOrg(this OptionSet options, Action action) 8 | { 9 | // If there is a single org, let's default to that org. 10 | var orgs = CacheManager.GetOrgCaches().ToArray(); 11 | if (orgs.Length == 1) 12 | action(Path.GetFileNameWithoutExtension(orgs[0].Name)); 13 | 14 | return options.Add("org=", "The {name} of the GitHub organization", action); 15 | } 16 | } -------------------------------------------------------------------------------- /src/policop/Program.cs: -------------------------------------------------------------------------------- 1 | using Mono.Options; 2 | 3 | namespace Microsoft.DotnetOrg.PolicyCop; 4 | 5 | internal static class Program 6 | { 7 | private static async Task Main(string[] args) 8 | { 9 | var commands = GetCommands(); 10 | var commandName = args.FirstOrDefault(); 11 | 12 | if (commandName is null || 13 | commandName == "-?" || 14 | commandName == "-h" || 15 | commandName == "--help") 16 | { 17 | var exeName = Path.GetFileNameWithoutExtension(Environment.GetCommandLineArgs()[0]); 18 | Console.Error.WriteLine($"usage: {exeName} [OPTIONS]+"); 19 | Console.Error.WriteLine(); 20 | 21 | var commandNameWidth = commands.Max(c => c.Name.Length) + 3; 22 | foreach (var c in commands) 23 | Console.Error.WriteLine($" {c.Name.PadRight(commandNameWidth)}{c.Description}"); 24 | return; 25 | } 26 | 27 | var command = commands.SingleOrDefault(c => c.Name == commandName); 28 | if (command is null) 29 | { 30 | Console.Error.WriteLine($"error: undefined command '{commandName}'"); 31 | return; 32 | } 33 | 34 | var help = false; 35 | 36 | var options = new OptionSet(); 37 | command.AddOptions(options); 38 | options.Add("h|?|help", null, v => help = true, true); 39 | options.Add(new ResponseFileSource()); 40 | 41 | try 42 | { 43 | var unprocessed = options.Parse(args.Skip(1)); 44 | 45 | if (help) 46 | { 47 | var exeName = Path.GetFileNameWithoutExtension(Environment.GetCommandLineArgs()[0]); 48 | Console.Error.WriteLine(command.Description); 49 | Console.Error.WriteLine($"usage: {exeName} {command.Name} [OPTIONS]+"); 50 | options.WriteOptionDescriptions(Console.Error); 51 | return; 52 | } 53 | 54 | if (unprocessed.Any()) 55 | { 56 | foreach (var option in unprocessed) 57 | Console.Error.WriteLine($"error: unrecognized argument {option}"); 58 | return; 59 | } 60 | } 61 | catch (Exception ex) 62 | { 63 | Console.Error.WriteLine(ex.ToString()); 64 | return; 65 | } 66 | 67 | await command.ExecuteAsync(); 68 | } 69 | 70 | private static IReadOnlyList GetCommands() 71 | { 72 | return typeof(Program).Assembly 73 | .GetTypes() 74 | .Where(t => typeof(ToolCommand).IsAssignableFrom(t) && 75 | !t.IsAbstract && t.GetConstructor(Array.Empty()) is not null) 76 | .Select(t => (ToolCommand?)Activator.CreateInstance(t)) 77 | .Where(t => t is not null) 78 | .Select(t => t!) 79 | .OrderBy(t => t.Name) 80 | .ToArray(); 81 | } 82 | } -------------------------------------------------------------------------------- /src/policop/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "GitHubPermissionPolicyChecker": { 4 | "policop": { 5 | "commandName": "Project", 6 | "commandLineArgs": "check --org dotnet --policy-repo dotnet/org-policy-violations -o P:\\org-policy\\violations.csv" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /src/policop/Reporting/OrgReportColumn.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | namespace Microsoft.DotnetOrg.PolicyCop.Reporting; 4 | 5 | internal sealed class OrgReportColumn : ReportColumn 6 | { 7 | private readonly Func _selector; 8 | 9 | public OrgReportColumn(string name, string description, Func selector) 10 | : base(name, description) 11 | { 12 | _selector = selector; 13 | } 14 | 15 | public override string? GetValue(ReportRow row) 16 | { 17 | return row.Org is null ? null : GetValue(row.Org); 18 | } 19 | 20 | public string GetValue(CachedOrg repo) 21 | { 22 | return _selector(repo); 23 | } 24 | } -------------------------------------------------------------------------------- /src/policop/Reporting/RepoReportColumn.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | namespace Microsoft.DotnetOrg.PolicyCop.Reporting; 4 | 5 | internal sealed class RepoReportColumn : ReportColumn 6 | { 7 | private readonly Func _selector; 8 | 9 | public RepoReportColumn(string name, string description, Func selector) 10 | : base(name, description) 11 | { 12 | _selector = selector; 13 | } 14 | 15 | public override string? GetValue(ReportRow row) 16 | { 17 | return row.Repo is null ? null : GetValue(row.Repo); 18 | } 19 | 20 | public string GetValue(CachedRepo repo) 21 | { 22 | return _selector(repo); 23 | } 24 | } -------------------------------------------------------------------------------- /src/policop/Reporting/ReportRow.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | namespace Microsoft.DotnetOrg.PolicyCop.Reporting; 4 | 5 | internal readonly struct ReportRow 6 | { 7 | public ReportRow(CachedRepo? repo = null, CachedTeam? team = null, CachedUser? user = null, CachedUserAccess? userAccess = null, CachedTeamAccess? teamAccess = null, CachedWhatIfPermission? whatIfPermission = null) 8 | { 9 | Repo = repo; 10 | Team = team; 11 | User = user; 12 | UserAccess = userAccess; 13 | TeamAccess = teamAccess; 14 | WhatIfPermission = whatIfPermission; 15 | } 16 | 17 | public CachedOrg? Org 18 | { 19 | get 20 | { 21 | if (Repo is not null) return Repo.Org; 22 | if (Team is not null) return Team.Org; 23 | if (User is not null) return User.Org; 24 | if (UserAccess is not null) return UserAccess.Org; 25 | if (TeamAccess is not null) return TeamAccess.Org; 26 | return null; 27 | } 28 | } 29 | 30 | public CachedRepo? Repo { get; } 31 | public CachedTeam? Team { get; } 32 | public CachedUser? User { get; } 33 | public CachedUserAccess? UserAccess { get; } 34 | public CachedTeamAccess? TeamAccess { get; } 35 | public CachedWhatIfPermission? WhatIfPermission { get; } 36 | } -------------------------------------------------------------------------------- /src/policop/Reporting/RowReportColumn.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.DotnetOrg.PolicyCop.Reporting; 2 | 3 | internal sealed class RowReportColumn : ReportColumn 4 | { 5 | private readonly Func _selector; 6 | 7 | public RowReportColumn(string name, string description, Func selector) 8 | : base(name, description) 9 | { 10 | _selector = selector; 11 | } 12 | 13 | public override string? GetValue(ReportRow row) 14 | { 15 | return _selector(row); 16 | } 17 | } -------------------------------------------------------------------------------- /src/policop/Reporting/TeamAccessReportColumn.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | namespace Microsoft.DotnetOrg.PolicyCop.Reporting; 4 | 5 | internal sealed class TeamAccessReportColumn : ReportColumn 6 | { 7 | private readonly Func _selector; 8 | 9 | public TeamAccessReportColumn(string name, string description, Func selector) 10 | : base(name, description) 11 | { 12 | _selector = selector; 13 | } 14 | 15 | public override string? GetValue(ReportRow row) 16 | { 17 | return row.TeamAccess is null ? null : GetValue(row.TeamAccess); 18 | } 19 | 20 | public string GetValue(CachedTeamAccess teamAccess) 21 | { 22 | return _selector(teamAccess); 23 | } 24 | } -------------------------------------------------------------------------------- /src/policop/Reporting/TeamReportColumn.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | namespace Microsoft.DotnetOrg.PolicyCop.Reporting; 4 | 5 | internal sealed class TeamReportColumn : ReportColumn 6 | { 7 | private readonly Func _selector; 8 | 9 | public TeamReportColumn(string name, string description, Func selector) 10 | : base(name, description) 11 | { 12 | _selector = selector; 13 | } 14 | 15 | public override string? GetValue(ReportRow row) 16 | { 17 | return row.Team is null ? null : GetValue(row.Team); 18 | } 19 | 20 | public string? GetValue(CachedTeam team) 21 | { 22 | return _selector(team); 23 | } 24 | } -------------------------------------------------------------------------------- /src/policop/Reporting/TeamUserReportColumn.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | namespace Microsoft.DotnetOrg.PolicyCop.Reporting; 4 | 5 | internal sealed class TeamUserReportColumn : ReportColumn 6 | { 7 | private readonly Func _selector; 8 | 9 | public TeamUserReportColumn(string name, string description, Func selector) 10 | : base(name, description) 11 | { 12 | _selector = selector; 13 | } 14 | 15 | public override string? GetValue(ReportRow row) 16 | { 17 | return row.Team is null || row.User is null ? null : GetValue(row.Team, row.User); 18 | } 19 | 20 | public string? GetValue(CachedTeam team, CachedUser user) 21 | { 22 | return _selector(team, user); 23 | } 24 | } -------------------------------------------------------------------------------- /src/policop/Reporting/UserAccessReportColumn.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | namespace Microsoft.DotnetOrg.PolicyCop.Reporting; 4 | 5 | internal sealed class UserAccessReportColumn : ReportColumn 6 | { 7 | private readonly Func _selector; 8 | 9 | public UserAccessReportColumn(string name, string description, Func selector) 10 | : base(name, description) 11 | { 12 | _selector = selector; 13 | } 14 | 15 | public override string? GetValue(ReportRow row) 16 | { 17 | return row.UserAccess is null ? null : GetValue(row.UserAccess); 18 | } 19 | 20 | public string? GetValue(CachedUserAccess userAccess) 21 | { 22 | return _selector(userAccess); 23 | } 24 | } -------------------------------------------------------------------------------- /src/policop/Reporting/UserReportColumn.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.DotnetOrg.GitHubCaching; 2 | 3 | namespace Microsoft.DotnetOrg.PolicyCop.Reporting; 4 | 5 | internal sealed class UserReportColumn : ReportColumn 6 | { 7 | private readonly Func _selector; 8 | 9 | public UserReportColumn(string name, string description, Func selector) 10 | : base(name, description) 11 | { 12 | _selector = selector; 13 | } 14 | 15 | public override string? GetValue(ReportRow row) 16 | { 17 | return row.User is null ? null : GetValue(row.User); 18 | } 19 | 20 | public string? GetValue(CachedUser user) 21 | { 22 | return _selector(user); 23 | } 24 | } -------------------------------------------------------------------------------- /src/policop/TableHelpers.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Csv; 2 | 3 | namespace Microsoft.DotnetOrg.PolicyCop; 4 | 5 | internal static class TableHelpers 6 | { 7 | public static void PrintToConsole(this CsvDocument document) 8 | { 9 | var indent = " "; 10 | var columnWidths = new int[document.Keys.Count]; 11 | 12 | for (var i = 0; i < columnWidths.Length; i++) 13 | columnWidths[i] = document.Keys[i].Length; 14 | 15 | foreach (var row in document.Rows) 16 | { 17 | for (var i = 0; i < columnWidths.Length; i++) 18 | { 19 | var key = document.Keys[i]; 20 | var text = row[key]; 21 | columnWidths[i] = Math.Max(columnWidths[i], text.Length); 22 | } 23 | } 24 | 25 | for (var i = 0; i < columnWidths.Length; i++) 26 | { 27 | Console.Write(indent); 28 | 29 | var text = document.Keys[i]; 30 | Console.Write(text.PadRight(columnWidths[i])); 31 | } 32 | 33 | Console.WriteLine(); 34 | 35 | for (var i = 0; i < columnWidths.Length; i++) 36 | { 37 | Console.Write(indent); 38 | 39 | var text = new string('-', columnWidths[i]); 40 | Console.Write(text); 41 | } 42 | 43 | Console.WriteLine(); 44 | 45 | foreach (var row in document.Rows) 46 | { 47 | for (var i = 0; i < columnWidths.Length; i++) 48 | { 49 | Console.Write(indent); 50 | 51 | var key = document.Keys[i]; 52 | var text = row[key]; 53 | Console.Write(text.PadRight(columnWidths[i])); 54 | } 55 | 56 | Console.WriteLine(); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/policop/ToolCommand.cs: -------------------------------------------------------------------------------- 1 | using Mono.Options; 2 | 3 | namespace Microsoft.DotnetOrg.PolicyCop; 4 | 5 | internal abstract class ToolCommand 6 | { 7 | public abstract string Name { get; } 8 | public abstract string Description { get; } 9 | public abstract void AddOptions(OptionSet options); 10 | public abstract Task ExecuteAsync(); 11 | } -------------------------------------------------------------------------------- /src/policop/policop.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | Microsoft.DotnetOrg.PolicyCop 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | true 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | --------------------------------------------------------------------------------