Skip to content

Commit 594acbe

Browse files
Copilotdavid3107
andcommitted
Add tag protection policy and evaluation logic
Co-authored-by: david3107 <[email protected]>
1 parent e0dc44a commit 594acbe

File tree

9 files changed

+367
-2
lines changed

9 files changed

+367
-2
lines changed

dist/evaluators/RepoPolicyEvaluator.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const WorkflowsChecks_1 = require("./repository/WorkflowsChecks");
1212
const RunnersChecks_1 = require("./repository/RunnersChecks");
1313
const WebHooksChecks_1 = require("./repository/WebHooksChecks");
1414
const AdminsChecks_1 = require("./repository/AdminsChecks");
15+
const TagProtectionChecks_1 = require("./repository/TagProtectionChecks");
1516
const outputFormatter_1 = require("../utils/outputFormatter");
1617
// This class is the main Repository evaluator. It evaluates the policy for a given repository.
1718
class RepoPolicyEvaluator {
@@ -91,6 +92,12 @@ class RepoPolicyEvaluator {
9192
logger_1.logger.debug(`Admins checks results: ${JSON.stringify(admins_checks)}`);
9293
this.repositoryCheckResults.push(admins_checks);
9394
}
95+
if (this.policy.protected_tags && this.policy.protected_tags.length > 0) {
96+
const tag_protection = new TagProtectionChecks_1.TagProtectionChecks(this.policy, this.repository);
97+
const tag_protection_results = await tag_protection.checkTagProtection();
98+
logger_1.logger.debug(`Tag protection rule results: ${JSON.stringify(tag_protection_results, null, 2)}`);
99+
this.repositoryCheckResults.push(tag_protection_results);
100+
}
94101
}
95102
// Run webhook checks
96103
printCheckResults() {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"use strict";
2+
Object.defineProperty(exports, "__esModule", { value: true });
3+
exports.TagProtectionChecks = void 0;
4+
const Repositories_1 = require("../../github/Repositories");
5+
class TagProtectionChecks {
6+
policy;
7+
repository;
8+
constructor(policy, repository) {
9+
this.policy = policy;
10+
this.repository = repository;
11+
}
12+
async checkTagProtection() {
13+
const rulesets = await (0, Repositories_1.getRepoRulesets)(this.repository.owner, this.repository.name);
14+
// Filter to get only tag rulesets that are active
15+
const tagRulesets = rulesets.filter((ruleset) => ruleset.target === "tag" && ruleset.enforcement === "active");
16+
const policyTags = this.policy.protected_tags || [];
17+
const protectedTagPatterns = tagRulesets
18+
.map((ruleset) => {
19+
// Extract tag patterns from conditions
20+
if (ruleset.conditions?.ref_name?.include) {
21+
return ruleset.conditions.ref_name.include;
22+
}
23+
return [];
24+
})
25+
.flat();
26+
// Check which policy tags are protected
27+
const passedTags = [];
28+
const failedTags = [];
29+
for (const policyTag of policyTags) {
30+
const isProtected = this.isTagProtected(policyTag.name, protectedTagPatterns);
31+
if (isProtected) {
32+
passedTags.push(policyTag.name);
33+
}
34+
else {
35+
failedTags.push(policyTag.name);
36+
}
37+
}
38+
return this.createResult(passedTags, failedTags, protectedTagPatterns);
39+
}
40+
isTagProtected(tagName, protectedPatterns) {
41+
// Check if tag name matches any protected pattern
42+
for (const pattern of protectedPatterns) {
43+
if (pattern === "~ALL") {
44+
return true;
45+
}
46+
// Convert GitHub pattern to regex
47+
const regexPattern = pattern
48+
.replace(/\*/g, ".*")
49+
.replace(/\?/g, ".")
50+
.replace(/\[/g, "\\[")
51+
.replace(/\]/g, "\\]");
52+
const regex = new RegExp(`^${regexPattern}$`);
53+
if (regex.test(tagName)) {
54+
return true;
55+
}
56+
}
57+
return false;
58+
}
59+
createResult(passed, failed, protectedPatterns) {
60+
const name = "Tag Protection";
61+
const pass = failed.length === 0;
62+
const data = {
63+
passed,
64+
failed: {
65+
not_protected: failed,
66+
},
67+
info: {
68+
protected_patterns: protectedPatterns,
69+
},
70+
};
71+
return { name, pass, data };
72+
}
73+
}
74+
exports.TagProtectionChecks = TagProtectionChecks;

dist/github/Repositories.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use strict";
22
Object.defineProperty(exports, "__esModule", { value: true });
3-
exports.getRepositoryCodeScanningAnalysis = exports.getRepoDependabotSecurityUpdates = exports.getRepoDependabotAlerts = exports.getRepoFile = exports.getRepoBranchProtection = exports.getRepoProtectedBranches = exports.getRepoBranch = exports.getRepoCollaborators = exports.getRepoPullRequests = exports.getRepository = exports.getRepositoriesForTeamAsAdmin = void 0;
3+
exports.getRepoRulesets = exports.getRepositoryCodeScanningAnalysis = exports.getRepoDependabotSecurityUpdates = exports.getRepoDependabotAlerts = exports.getRepoFile = exports.getRepoBranchProtection = exports.getRepoProtectedBranches = exports.getRepoBranch = exports.getRepoCollaborators = exports.getRepoPullRequests = exports.getRepository = exports.getRepositoriesForTeamAsAdmin = void 0;
44
const GitArmorKit_1 = require("./GitArmorKit");
55
const logger_1 = require("../utils/logger");
66
const getRepositoriesForTeamAsAdmin = async (org, teamSlug) => {
@@ -151,3 +151,24 @@ const getRepositoryCodeScanningAnalysis = async (owner, repo) => {
151151
}
152152
};
153153
exports.getRepositoryCodeScanningAnalysis = getRepositoryCodeScanningAnalysis;
154+
// get repository rulesets for tag protection
155+
const getRepoRulesets = async (owner, repo) => {
156+
const octokit = new GitArmorKit_1.GitArmorKit();
157+
try {
158+
const response = await octokit.rest.repos.getRepoRulesets({
159+
owner: owner,
160+
repo: repo,
161+
});
162+
return response.data;
163+
}
164+
catch (error) {
165+
logger_1.logger.debug(`Repository rulesets fetching error: ${error.message}`);
166+
if (error.status === 404) {
167+
return [];
168+
}
169+
else {
170+
throw error;
171+
}
172+
}
173+
};
174+
exports.getRepoRulesets = getRepoRulesets;

dist/index.js

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47885,6 +47885,7 @@ const WorkflowsChecks_1 = __nccwpck_require__(8336);
4788547885
const RunnersChecks_1 = __nccwpck_require__(9863);
4788647886
const WebHooksChecks_1 = __nccwpck_require__(1149);
4788747887
const AdminsChecks_1 = __nccwpck_require__(2818);
47888+
const TagProtectionChecks_1 = __nccwpck_require__(3739);
4788847889
const outputFormatter_1 = __nccwpck_require__(6871);
4788947890
// This class is the main Repository evaluator. It evaluates the policy for a given repository.
4789047891
class RepoPolicyEvaluator {
@@ -47964,6 +47965,12 @@ class RepoPolicyEvaluator {
4796447965
logger_1.logger.debug(`Admins checks results: ${JSON.stringify(admins_checks)}`);
4796547966
this.repositoryCheckResults.push(admins_checks);
4796647967
}
47968+
if (this.policy.protected_tags && this.policy.protected_tags.length > 0) {
47969+
const tag_protection = new TagProtectionChecks_1.TagProtectionChecks(this.policy, this.repository);
47970+
const tag_protection_results = await tag_protection.checkTagProtection();
47971+
logger_1.logger.debug(`Tag protection rule results: ${JSON.stringify(tag_protection_results, null, 2)}`);
47972+
this.repositoryCheckResults.push(tag_protection_results);
47973+
}
4796747974
}
4796847975
// Run webhook checks
4796947976
printCheckResults() {
@@ -49215,6 +49222,88 @@ class RunnersChecks {
4921549222
exports.RunnersChecks = RunnersChecks;
4921649223

4921749224

49225+
/***/ }),
49226+
49227+
/***/ 3739:
49228+
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
49229+
49230+
"use strict";
49231+
49232+
Object.defineProperty(exports, "__esModule", ({ value: true }));
49233+
exports.TagProtectionChecks = void 0;
49234+
const Repositories_1 = __nccwpck_require__(3354);
49235+
class TagProtectionChecks {
49236+
policy;
49237+
repository;
49238+
constructor(policy, repository) {
49239+
this.policy = policy;
49240+
this.repository = repository;
49241+
}
49242+
async checkTagProtection() {
49243+
const rulesets = await (0, Repositories_1.getRepoRulesets)(this.repository.owner, this.repository.name);
49244+
// Filter to get only tag rulesets that are active
49245+
const tagRulesets = rulesets.filter((ruleset) => ruleset.target === "tag" && ruleset.enforcement === "active");
49246+
const policyTags = this.policy.protected_tags || [];
49247+
const protectedTagPatterns = tagRulesets
49248+
.map((ruleset) => {
49249+
// Extract tag patterns from conditions
49250+
if (ruleset.conditions?.ref_name?.include) {
49251+
return ruleset.conditions.ref_name.include;
49252+
}
49253+
return [];
49254+
})
49255+
.flat();
49256+
// Check which policy tags are protected
49257+
const passedTags = [];
49258+
const failedTags = [];
49259+
for (const policyTag of policyTags) {
49260+
const isProtected = this.isTagProtected(policyTag.name, protectedTagPatterns);
49261+
if (isProtected) {
49262+
passedTags.push(policyTag.name);
49263+
}
49264+
else {
49265+
failedTags.push(policyTag.name);
49266+
}
49267+
}
49268+
return this.createResult(passedTags, failedTags, protectedTagPatterns);
49269+
}
49270+
isTagProtected(tagName, protectedPatterns) {
49271+
// Check if tag name matches any protected pattern
49272+
for (const pattern of protectedPatterns) {
49273+
if (pattern === "~ALL") {
49274+
return true;
49275+
}
49276+
// Convert GitHub pattern to regex
49277+
const regexPattern = pattern
49278+
.replace(/\*/g, ".*")
49279+
.replace(/\?/g, ".")
49280+
.replace(/\[/g, "\\[")
49281+
.replace(/\]/g, "\\]");
49282+
const regex = new RegExp(`^${regexPattern}$`);
49283+
if (regex.test(tagName)) {
49284+
return true;
49285+
}
49286+
}
49287+
return false;
49288+
}
49289+
createResult(passed, failed, protectedPatterns) {
49290+
const name = "Tag Protection";
49291+
const pass = failed.length === 0;
49292+
const data = {
49293+
passed,
49294+
failed: {
49295+
not_protected: failed,
49296+
},
49297+
info: {
49298+
protected_patterns: protectedPatterns,
49299+
},
49300+
};
49301+
return { name, pass, data };
49302+
}
49303+
}
49304+
exports.TagProtectionChecks = TagProtectionChecks;
49305+
49306+
4921849307
/***/ }),
4921949308

4922049309
/***/ 1149:
@@ -49656,7 +49745,7 @@ exports.getCustomRolesForOrg = getCustomRolesForOrg;
4965649745
"use strict";
4965749746

4965849747
Object.defineProperty(exports, "__esModule", ({ value: true }));
49659-
exports.getRepositoryCodeScanningAnalysis = exports.getRepoDependabotSecurityUpdates = exports.getRepoDependabotAlerts = exports.getRepoFile = exports.getRepoBranchProtection = exports.getRepoProtectedBranches = exports.getRepoBranch = exports.getRepoCollaborators = exports.getRepoPullRequests = exports.getRepository = exports.getRepositoriesForTeamAsAdmin = void 0;
49748+
exports.getRepoRulesets = exports.getRepositoryCodeScanningAnalysis = exports.getRepoDependabotSecurityUpdates = exports.getRepoDependabotAlerts = exports.getRepoFile = exports.getRepoBranchProtection = exports.getRepoProtectedBranches = exports.getRepoBranch = exports.getRepoCollaborators = exports.getRepoPullRequests = exports.getRepository = exports.getRepositoriesForTeamAsAdmin = void 0;
4966049749
const GitArmorKit_1 = __nccwpck_require__(2009);
4966149750
const logger_1 = __nccwpck_require__(8836);
4966249751
const getRepositoriesForTeamAsAdmin = async (org, teamSlug) => {
@@ -49807,6 +49896,27 @@ const getRepositoryCodeScanningAnalysis = async (owner, repo) => {
4980749896
}
4980849897
};
4980949898
exports.getRepositoryCodeScanningAnalysis = getRepositoryCodeScanningAnalysis;
49899+
// get repository rulesets for tag protection
49900+
const getRepoRulesets = async (owner, repo) => {
49901+
const octokit = new GitArmorKit_1.GitArmorKit();
49902+
try {
49903+
const response = await octokit.rest.repos.getRepoRulesets({
49904+
owner: owner,
49905+
repo: repo,
49906+
});
49907+
return response.data;
49908+
}
49909+
catch (error) {
49910+
logger_1.logger.debug(`Repository rulesets fetching error: ${error.message}`);
49911+
if (error.status === 404) {
49912+
return [];
49913+
}
49914+
else {
49915+
throw error;
49916+
}
49917+
}
49918+
};
49919+
exports.getRepoRulesets = getRepoRulesets;
4981049920

4981149921

4981249922
/***/ }),

policies/repository.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ protected_branches:
3434
required_pull_request_reviews: true
3535
allow_fork_syncing: false
3636

37+
# define the protected tags for the repository
38+
protected_tags:
39+
- name: v*
40+
- name: release-*
41+
- name: prod-*
42+
3743

3844
file_exists:
3945
- .github/CODEOWNERS

src/evaluators/RepoPolicyEvaluator.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { WorkflowsChecks } from "./repository/WorkflowsChecks";
1010
import { RunnersChecks } from "./repository/RunnersChecks";
1111
import { WebHooksChecks } from "./repository/WebHooksChecks";
1212
import { AdminsChecks } from "./repository/AdminsChecks";
13+
import { TagProtectionChecks } from "./repository/TagProtectionChecks";
1314
import {
1415
printEnhancedCheckResult,
1516
printResultsHeader,
@@ -159,6 +160,22 @@ export class RepoPolicyEvaluator {
159160
logger.debug(`Admins checks results: ${JSON.stringify(admins_checks)}`);
160161
this.repositoryCheckResults.push(admins_checks);
161162
}
163+
164+
if (this.policy.protected_tags && this.policy.protected_tags.length > 0) {
165+
const tag_protection = new TagProtectionChecks(
166+
this.policy,
167+
this.repository,
168+
);
169+
const tag_protection_results = await tag_protection.checkTagProtection();
170+
logger.debug(
171+
`Tag protection rule results: ${JSON.stringify(
172+
tag_protection_results,
173+
null,
174+
2,
175+
)}`,
176+
);
177+
this.repositoryCheckResults.push(tag_protection_results);
178+
}
162179
}
163180

164181
// Run webhook checks

0 commit comments

Comments
 (0)