├── .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 | [](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