From 9adf2f79dbe3b1509c5a3aace3477dd6f8ade61b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 06:57:31 +0000 Subject: [PATCH 1/3] Initial plan From 562353d4a809ca3bc1f10f8c9f77208b777cdfd0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 07:05:21 +0000 Subject: [PATCH 2/3] Add sha_pinning_required check for repository and organization actions Co-authored-by: david3107 <20040740+david3107@users.noreply.github.com> --- dist/evaluators/OrgPolicyEvaluator.js | 7 + .../organization/OrgActionsChecks.js | 52 ++++++++ dist/evaluators/repository/ActionsChecks.js | 37 ++++-- dist/github/Actions.js | 17 ++- dist/index.js | 121 ++++++++++++++++-- policies/organization.readme.md | 23 ++++ policies/organization.yml | 6 + policies/repository.readme.md | 3 + policies/repository.yml | 1 + src/evaluators/OrgPolicyEvaluator.ts | 11 ++ .../organization/OrgActionsChecks.ts | 67 ++++++++++ src/evaluators/repository/ActionsChecks.ts | 44 ++++++- src/github/Actions.ts | 21 +++ src/types/common/main.d.ts | 6 + 14 files changed, 394 insertions(+), 22 deletions(-) create mode 100644 dist/evaluators/organization/OrgActionsChecks.js create mode 100644 src/evaluators/organization/OrgActionsChecks.ts diff --git a/dist/evaluators/OrgPolicyEvaluator.js b/dist/evaluators/OrgPolicyEvaluator.js index abc437a..6329d79 100644 --- a/dist/evaluators/OrgPolicyEvaluator.js +++ b/dist/evaluators/OrgPolicyEvaluator.js @@ -5,6 +5,7 @@ const logger_1 = require("../utils/logger"); const OrgGHASChecks_1 = require("./organization/OrgGHASChecks"); const OrgAuthenticationChecks_1 = require("./organization/OrgAuthenticationChecks"); const OrgCustomRolesChecks_1 = require("./organization/OrgCustomRolesChecks"); +const OrgActionsChecks_1 = require("./organization/OrgActionsChecks"); const Organization_1 = require("../github/Organization"); const PrivilegesChecks_1 = require("./organization/PrivilegesChecks"); class OrgPolicyEvaluator { @@ -48,6 +49,12 @@ class OrgPolicyEvaluator { logger_1.logger.debug(`Org Custom Roles results: ${JSON.stringify(custom_roles_checks)}`); this.orgCheckResults.push(custom_roles_checks); } + // check organization actions settings + if (this.policy.actions) { + const actions_checks = await new OrgActionsChecks_1.OrgActionsChecks(this.policy, this.organization).evaluate(); + logger_1.logger.debug(`Org Actions results: ${JSON.stringify(actions_checks)}`); + this.orgCheckResults.push(actions_checks); + } } printCheckResults() { logger_1.logger.info("------------------------------------------------------------------------"); diff --git a/dist/evaluators/organization/OrgActionsChecks.js b/dist/evaluators/organization/OrgActionsChecks.js new file mode 100644 index 0000000..a1f1ba2 --- /dev/null +++ b/dist/evaluators/organization/OrgActionsChecks.js @@ -0,0 +1,52 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.OrgActionsChecks = void 0; +const Actions_1 = require("../../github/Actions"); +class OrgActionsChecks { + policy; + organization; + constructor(policy, organization) { + this.policy = policy; + this.organization = organization; + } + async evaluate() { + const actionsPermissions = await (0, Actions_1.getOrgActionsPermissions)(this.organization.name); + const checks = { + enabled_repositories: this.checkEnabledRepositories(actionsPermissions.enabled_repositories), + allowed_actions: this.checkAllowedActions(actionsPermissions.allowed_actions), + sha_pinning_required: this.checkShaPinningRequired(actionsPermissions.sha_pinning_required), + }; + const name = "Org Actions Checks"; + const pass = Object.values(checks).every((check) => check === true); + const data = { + enabled_repositories_github: actionsPermissions.enabled_repositories, + enabled_repositories_policy: this.policy.actions.enabled_repositories, + allowed_actions_github: actionsPermissions.allowed_actions, + allowed_actions_policy: this.policy.actions.allowed_actions, + sha_pinning_required_github: actionsPermissions + .sha_pinning_required, + sha_pinning_required_policy: this.policy.actions.sha_pinning_required, + ...checks, + }; + return { name, pass, data }; + } + checkEnabledRepositories(githubValue) { + if (this.policy.actions.enabled_repositories === undefined) { + return true; + } + return githubValue === this.policy.actions.enabled_repositories; + } + checkAllowedActions(githubValue) { + if (this.policy.actions.allowed_actions === undefined) { + return true; + } + return githubValue === this.policy.actions.allowed_actions; + } + checkShaPinningRequired(githubValue) { + if (this.policy.actions.sha_pinning_required === undefined) { + return true; + } + return githubValue === this.policy.actions.sha_pinning_required; + } +} +exports.OrgActionsChecks = OrgActionsChecks; diff --git a/dist/evaluators/repository/ActionsChecks.js b/dist/evaluators/repository/ActionsChecks.js index bd21911..5d51fa8 100644 --- a/dist/evaluators/repository/ActionsChecks.js +++ b/dist/evaluators/repository/ActionsChecks.js @@ -16,17 +16,19 @@ class ActionsChecks { const actionsPermissionsResult = actionsPermissions.enabled; const actionsPermissionsAllowedActions = actionsPermissions.allowed_actions; const actionsPermissionsPolicy = this.policy.allowed_actions.permission; + const shaPinningRequired = actionsPermissions.sha_pinning_required; + const shaPinningRequiredPolicy = this.policy.allowed_actions.sha_pinning_required; if (!actionsPermissionsResult) { - return this.createResult(actionsPermissionsPolicy === "none", "none", actionsPermissionsPolicy); + return this.createResult(actionsPermissionsPolicy === "none", "none", actionsPermissionsPolicy, shaPinningRequired, shaPinningRequiredPolicy); } switch (actionsPermissionsPolicy) { case "selected": if (!this.policy.allowed_actions.selected.patterns_allowed) { logger_1.logger.error("error: the policy (.yml) should have the list of patterns_allowed when permission is 'selected'"); - return this.createResult(false, actionsPermissionsAllowedActions, actionsPermissionsPolicy); + return this.createResult(false, actionsPermissionsAllowedActions, actionsPermissionsPolicy, shaPinningRequired, shaPinningRequiredPolicy); } if (actionsPermissionsAllowedActions !== "selected") - return this.createResult(false, actionsPermissionsAllowedActions, actionsPermissionsPolicy); + return this.createResult(false, actionsPermissionsAllowedActions, actionsPermissionsPolicy, shaPinningRequired, shaPinningRequiredPolicy); const selectedActions = await (0, Actions_1.getRepoSelectedActions)(this.repository.owner, this.repository.name); const githubOwnedAllowedActions = selectedActions.github_owned_allowed; const verifiedAllowedActions = selectedActions.verified_allowed; @@ -36,25 +38,30 @@ class ActionsChecks { githubOwnedAllowedActions; const verifiedAllowedMatchesPolicy = this.policy.allowed_actions.selected.verified_allowed === verifiedAllowedActions; - return this.createResultSelected(selectedActionsAllowed, actionsPermissionsAllowedActions, githubOwnedAllowedMatchesPolicy, verifiedAllowedMatchesPolicy, patternsAllowedActions, this.policy.allowed_actions.selected.patterns_allowed); + return this.createResultSelected(selectedActionsAllowed, actionsPermissionsAllowedActions, githubOwnedAllowedMatchesPolicy, verifiedAllowedMatchesPolicy, patternsAllowedActions, this.policy.allowed_actions.selected.patterns_allowed, shaPinningRequired, shaPinningRequiredPolicy); case "all": case "local_only": case "none": - return this.createResult(actionsPermissionsPolicy === actionsPermissionsAllowedActions, actionsPermissionsAllowedActions, actionsPermissionsPolicy); + return this.createResult(actionsPermissionsPolicy === actionsPermissionsAllowedActions, actionsPermissionsAllowedActions, actionsPermissionsPolicy, shaPinningRequired, shaPinningRequiredPolicy); default: logger_1.logger.error(`error: invalid policy value '${actionsPermissionsPolicy}'. It should be one of ${POLICY_VALUES.join(", ")}.`); } } - createResult(actions_permissions, github_allowed_actions, policy_allowed_actions) { + createResult(actions_permissions, github_allowed_actions, policy_allowed_actions, sha_pinning_required, sha_pinning_required_policy) { let name = "Actions Check"; let pass = false; let data = {}; - if (actions_permissions) { + // Check sha_pinning_required if it's defined in the policy + const shaPinningMatches = sha_pinning_required_policy === undefined || + sha_pinning_required === sha_pinning_required_policy; + if (actions_permissions && shaPinningMatches) { pass = true; data = { actions_permissions, github_allowed_actions, policy_allowed_actions, + sha_pinning_required, + sha_pinning_required_policy, }; } else { @@ -62,15 +69,23 @@ class ActionsChecks { actions_permissions, github_allowed_actions, policy_allowed_actions, + sha_pinning_required, + sha_pinning_required_policy, }; } return { name, pass, data }; } - createResultSelected(actions_permissions, github_allowed_actions, github_owned_allowed, verified_allowed, patterns_allowed_github, patterns_allowed_policy) { + createResultSelected(actions_permissions, github_allowed_actions, github_owned_allowed, verified_allowed, patterns_allowed_github, patterns_allowed_policy, sha_pinning_required, sha_pinning_required_policy) { let name = "Actions Check"; let pass = false; let data = {}; - if (actions_permissions && github_owned_allowed && verified_allowed) { + // Check sha_pinning_required if it's defined in the policy + const shaPinningMatches = sha_pinning_required_policy === undefined || + sha_pinning_required === sha_pinning_required_policy; + if (actions_permissions && + github_owned_allowed && + verified_allowed && + shaPinningMatches) { pass = true; data = { actions_permissions, @@ -79,6 +94,8 @@ class ActionsChecks { verified_allowed, patterns_allowed_github, patterns_allowed_policy, + sha_pinning_required, + sha_pinning_required_policy, }; } else { @@ -89,6 +106,8 @@ class ActionsChecks { verified_allowed, patterns_allowed_github, patterns_allowed_policy, + sha_pinning_required, + sha_pinning_required_policy, }; } return { diff --git a/dist/github/Actions.js b/dist/github/Actions.js index 4f39365..ec924ed 100644 --- a/dist/github/Actions.js +++ b/dist/github/Actions.js @@ -1,8 +1,23 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.getRepoWorkflowActions = exports.getRepoWorkflows = exports.getRepoWorkflowAccessPermissions = exports.getRepoDefaultWorkflowsPermissions = exports.getRepoSelectedActions = exports.getRepoActionsPermissions = void 0; +exports.getRepoWorkflowActions = exports.getRepoWorkflows = exports.getRepoWorkflowAccessPermissions = exports.getRepoDefaultWorkflowsPermissions = exports.getRepoSelectedActions = exports.getRepoActionsPermissions = exports.getOrgActionsPermissions = void 0; const GitArmorKit_1 = require("./GitArmorKit"); const logger_1 = require("../utils/logger"); +//Get GitHub Actions permissions for an organization +const getOrgActionsPermissions = async (org) => { + try { + const octokit = new GitArmorKit_1.GitArmorKit(); + const response = await octokit.rest.actions.getGithubActionsPermissionsOrganization({ + org: org, + }); + return response.data; + } + catch (error) { + logger_1.logger.error(error.message); + throw error; + } +}; +exports.getOrgActionsPermissions = getOrgActionsPermissions; //Get GitHub Actions permissions for a repository const getRepoActionsPermissions = async (owner, repo) => { try { diff --git a/dist/index.js b/dist/index.js index 17132b3..670e11b 100644 --- a/dist/index.js +++ b/dist/index.js @@ -47797,6 +47797,7 @@ const logger_1 = __nccwpck_require__(8836); const OrgGHASChecks_1 = __nccwpck_require__(7137); const OrgAuthenticationChecks_1 = __nccwpck_require__(7124); const OrgCustomRolesChecks_1 = __nccwpck_require__(8731); +const OrgActionsChecks_1 = __nccwpck_require__(9582); const Organization_1 = __nccwpck_require__(7155); const PrivilegesChecks_1 = __nccwpck_require__(3922); class OrgPolicyEvaluator { @@ -47840,6 +47841,12 @@ class OrgPolicyEvaluator { logger_1.logger.debug(`Org Custom Roles results: ${JSON.stringify(custom_roles_checks)}`); this.orgCheckResults.push(custom_roles_checks); } + // check organization actions settings + if (this.policy.actions) { + const actions_checks = await new OrgActionsChecks_1.OrgActionsChecks(this.policy, this.organization).evaluate(); + logger_1.logger.debug(`Org Actions results: ${JSON.stringify(actions_checks)}`); + this.orgCheckResults.push(actions_checks); + } } printCheckResults() { logger_1.logger.info("------------------------------------------------------------------------"); @@ -48020,6 +48027,66 @@ class FilesExistChecks { exports.FilesExistChecks = FilesExistChecks; +/***/ }), + +/***/ 9582: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.OrgActionsChecks = void 0; +const Actions_1 = __nccwpck_require__(5248); +class OrgActionsChecks { + policy; + organization; + constructor(policy, organization) { + this.policy = policy; + this.organization = organization; + } + async evaluate() { + const actionsPermissions = await (0, Actions_1.getOrgActionsPermissions)(this.organization.name); + const checks = { + enabled_repositories: this.checkEnabledRepositories(actionsPermissions.enabled_repositories), + allowed_actions: this.checkAllowedActions(actionsPermissions.allowed_actions), + sha_pinning_required: this.checkShaPinningRequired(actionsPermissions.sha_pinning_required), + }; + const name = "Org Actions Checks"; + const pass = Object.values(checks).every((check) => check === true); + const data = { + enabled_repositories_github: actionsPermissions.enabled_repositories, + enabled_repositories_policy: this.policy.actions.enabled_repositories, + allowed_actions_github: actionsPermissions.allowed_actions, + allowed_actions_policy: this.policy.actions.allowed_actions, + sha_pinning_required_github: actionsPermissions + .sha_pinning_required, + sha_pinning_required_policy: this.policy.actions.sha_pinning_required, + ...checks, + }; + return { name, pass, data }; + } + checkEnabledRepositories(githubValue) { + if (this.policy.actions.enabled_repositories === undefined) { + return true; + } + return githubValue === this.policy.actions.enabled_repositories; + } + checkAllowedActions(githubValue) { + if (this.policy.actions.allowed_actions === undefined) { + return true; + } + return githubValue === this.policy.actions.allowed_actions; + } + checkShaPinningRequired(githubValue) { + if (this.policy.actions.sha_pinning_required === undefined) { + return true; + } + return githubValue === this.policy.actions.sha_pinning_required; + } +} +exports.OrgActionsChecks = OrgActionsChecks; + + /***/ }), /***/ 7124: @@ -48318,17 +48385,19 @@ class ActionsChecks { const actionsPermissionsResult = actionsPermissions.enabled; const actionsPermissionsAllowedActions = actionsPermissions.allowed_actions; const actionsPermissionsPolicy = this.policy.allowed_actions.permission; + const shaPinningRequired = actionsPermissions.sha_pinning_required; + const shaPinningRequiredPolicy = this.policy.allowed_actions.sha_pinning_required; if (!actionsPermissionsResult) { - return this.createResult(actionsPermissionsPolicy === "none", "none", actionsPermissionsPolicy); + return this.createResult(actionsPermissionsPolicy === "none", "none", actionsPermissionsPolicy, shaPinningRequired, shaPinningRequiredPolicy); } switch (actionsPermissionsPolicy) { case "selected": if (!this.policy.allowed_actions.selected.patterns_allowed) { logger_1.logger.error("error: the policy (.yml) should have the list of patterns_allowed when permission is 'selected'"); - return this.createResult(false, actionsPermissionsAllowedActions, actionsPermissionsPolicy); + return this.createResult(false, actionsPermissionsAllowedActions, actionsPermissionsPolicy, shaPinningRequired, shaPinningRequiredPolicy); } if (actionsPermissionsAllowedActions !== "selected") - return this.createResult(false, actionsPermissionsAllowedActions, actionsPermissionsPolicy); + return this.createResult(false, actionsPermissionsAllowedActions, actionsPermissionsPolicy, shaPinningRequired, shaPinningRequiredPolicy); const selectedActions = await (0, Actions_1.getRepoSelectedActions)(this.repository.owner, this.repository.name); const githubOwnedAllowedActions = selectedActions.github_owned_allowed; const verifiedAllowedActions = selectedActions.verified_allowed; @@ -48338,25 +48407,30 @@ class ActionsChecks { githubOwnedAllowedActions; const verifiedAllowedMatchesPolicy = this.policy.allowed_actions.selected.verified_allowed === verifiedAllowedActions; - return this.createResultSelected(selectedActionsAllowed, actionsPermissionsAllowedActions, githubOwnedAllowedMatchesPolicy, verifiedAllowedMatchesPolicy, patternsAllowedActions, this.policy.allowed_actions.selected.patterns_allowed); + return this.createResultSelected(selectedActionsAllowed, actionsPermissionsAllowedActions, githubOwnedAllowedMatchesPolicy, verifiedAllowedMatchesPolicy, patternsAllowedActions, this.policy.allowed_actions.selected.patterns_allowed, shaPinningRequired, shaPinningRequiredPolicy); case "all": case "local_only": case "none": - return this.createResult(actionsPermissionsPolicy === actionsPermissionsAllowedActions, actionsPermissionsAllowedActions, actionsPermissionsPolicy); + return this.createResult(actionsPermissionsPolicy === actionsPermissionsAllowedActions, actionsPermissionsAllowedActions, actionsPermissionsPolicy, shaPinningRequired, shaPinningRequiredPolicy); default: logger_1.logger.error(`error: invalid policy value '${actionsPermissionsPolicy}'. It should be one of ${POLICY_VALUES.join(", ")}.`); } } - createResult(actions_permissions, github_allowed_actions, policy_allowed_actions) { + createResult(actions_permissions, github_allowed_actions, policy_allowed_actions, sha_pinning_required, sha_pinning_required_policy) { let name = "Actions Check"; let pass = false; let data = {}; - if (actions_permissions) { + // Check sha_pinning_required if it's defined in the policy + const shaPinningMatches = sha_pinning_required_policy === undefined || + sha_pinning_required === sha_pinning_required_policy; + if (actions_permissions && shaPinningMatches) { pass = true; data = { actions_permissions, github_allowed_actions, policy_allowed_actions, + sha_pinning_required, + sha_pinning_required_policy, }; } else { @@ -48364,15 +48438,23 @@ class ActionsChecks { actions_permissions, github_allowed_actions, policy_allowed_actions, + sha_pinning_required, + sha_pinning_required_policy, }; } return { name, pass, data }; } - createResultSelected(actions_permissions, github_allowed_actions, github_owned_allowed, verified_allowed, patterns_allowed_github, patterns_allowed_policy) { + createResultSelected(actions_permissions, github_allowed_actions, github_owned_allowed, verified_allowed, patterns_allowed_github, patterns_allowed_policy, sha_pinning_required, sha_pinning_required_policy) { let name = "Actions Check"; let pass = false; let data = {}; - if (actions_permissions && github_owned_allowed && verified_allowed) { + // Check sha_pinning_required if it's defined in the policy + const shaPinningMatches = sha_pinning_required_policy === undefined || + sha_pinning_required === sha_pinning_required_policy; + if (actions_permissions && + github_owned_allowed && + verified_allowed && + shaPinningMatches) { pass = true; data = { actions_permissions, @@ -48381,6 +48463,8 @@ class ActionsChecks { verified_allowed, patterns_allowed_github, patterns_allowed_policy, + sha_pinning_required, + sha_pinning_required_policy, }; } else { @@ -48391,6 +48475,8 @@ class ActionsChecks { verified_allowed, patterns_allowed_github, patterns_allowed_policy, + sha_pinning_required, + sha_pinning_required_policy, }; } return { @@ -49091,9 +49177,24 @@ exports.WorkflowsChecks = WorkflowsChecks; "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.getRepoWorkflowActions = exports.getRepoWorkflows = exports.getRepoWorkflowAccessPermissions = exports.getRepoDefaultWorkflowsPermissions = exports.getRepoSelectedActions = exports.getRepoActionsPermissions = void 0; +exports.getRepoWorkflowActions = exports.getRepoWorkflows = exports.getRepoWorkflowAccessPermissions = exports.getRepoDefaultWorkflowsPermissions = exports.getRepoSelectedActions = exports.getRepoActionsPermissions = exports.getOrgActionsPermissions = void 0; const GitArmorKit_1 = __nccwpck_require__(2009); const logger_1 = __nccwpck_require__(8836); +//Get GitHub Actions permissions for an organization +const getOrgActionsPermissions = async (org) => { + try { + const octokit = new GitArmorKit_1.GitArmorKit(); + const response = await octokit.rest.actions.getGithubActionsPermissionsOrganization({ + org: org, + }); + return response.data; + } + catch (error) { + logger_1.logger.error(error.message); + throw error; + } +}; +exports.getOrgActionsPermissions = getOrgActionsPermissions; //Get GitHub Actions permissions for a repository const getRepoActionsPermissions = async (owner, repo) => { try { diff --git a/policies/organization.readme.md b/policies/organization.readme.md index bafebb5..9efb9b6 100644 --- a/policies/organization.readme.md +++ b/policies/organization.readme.md @@ -126,6 +126,29 @@ authentication: - `false`: Multi-Factor authentication is not required. +## Actions + +This policy checks the GitHub Actions permissions for the organization to ensure that actions usage is properly controlled. [GitHub](https://docs.github.com/en/rest/actions/permissions?apiVersion=2022-11-28#get-github-actions-permissions-for-an-organization) + +```yml +actions: + enabled_repositories: all # all, none, selected + allowed_actions: all # all, local_only, selected + sha_pinning_required: true +``` + +- `enabled_repositories` (**optional**): specifies which repositories in the organization can use GitHub Actions. The options are: + - `all`: all repositories can use GitHub Actions. + - `none`: no repositories can use GitHub Actions. + - `selected`: only selected repositories can use GitHub Actions. + +- `allowed_actions` (**optional**): specifies which actions and reusable workflows can be run. The options are: + - `all`: any action or reusable workflow can be used. + - `local_only`: only actions and reusable workflows defined in the organization can be used. + - `selected`: only selected actions and reusable workflows can be used (additional configuration required). + +- `sha_pinning_required` (**optional**): if set to `true`, SHA pinning is required for actions in workflows. When enabled, actions must be referenced by their full commit SHA rather than by tag or branch. This improves security by ensuring that the exact version of an action is used and preventing potential supply chain attacks. + ## GHAS (Github Advanced Security) diff --git a/policies/organization.yml b/policies/organization.yml index 1d82326..1882082 100644 --- a/policies/organization.yml +++ b/policies/organization.yml @@ -28,6 +28,12 @@ member_privileges: authentication: mfa_required: true +# Actions +actions: + enabled_repositories: all # all, none, selected + allowed_actions: all # all, local_only, selected + sha_pinning_required: true + # Advanced Security advanced_security: automatic_dependency_graph: true diff --git a/policies/repository.readme.md b/policies/repository.readme.md index 6bbbb9c..3c7c20a 100644 --- a/policies/repository.readme.md +++ b/policies/repository.readme.md @@ -75,6 +75,7 @@ allowed_actions: patterns_allowed: - "veracode/*" - "dcodx/*" + sha_pinning_required: true ``` `permission` can have the following values: @@ -92,6 +93,8 @@ When `permission` is set to `selected`, the following options are available: When `permission` is set to `local_only`, `all` or `none`, the `selected` section is ignored. +`sha_pinning_required` (**optional**): if set to `true`, SHA pinning is required for actions in workflows. When enabled, actions must be referenced by their full commit SHA rather than by tag or branch. This improves security by ensuring that the exact version of an action is used and preventing potential supply chain attacks. + ## GHAS (GitHub Advanced Security) The policy checks the GHAS settings for the specified repository. diff --git a/policies/repository.yml b/policies/repository.yml index 1a4f90d..30cb334 100644 --- a/policies/repository.yml +++ b/policies/repository.yml @@ -59,6 +59,7 @@ allowed_actions: patterns_allowed: - "veracode/*" - "dcodx/*" + sha_pinning_required: true workflows: permission: read # read, write diff --git a/src/evaluators/OrgPolicyEvaluator.ts b/src/evaluators/OrgPolicyEvaluator.ts index b50743d..ce3b923 100644 --- a/src/evaluators/OrgPolicyEvaluator.ts +++ b/src/evaluators/OrgPolicyEvaluator.ts @@ -3,6 +3,7 @@ import { OrgPolicy, Organization, CheckResult } from "../types/common/main"; import { OrgGHASChecks } from "./organization/OrgGHASChecks"; import { OrgAuthenticationChecks } from "./organization/OrgAuthenticationChecks"; import { OrgCustomRolesChecks } from "./organization/OrgCustomRolesChecks"; +import { OrgActionsChecks } from "./organization/OrgActionsChecks"; import { getOrganization } from "../github/Organization"; import { PrivilegesChecks } from "./organization/PrivilegesChecks"; @@ -77,6 +78,16 @@ export class OrgPolicyEvaluator { ); this.orgCheckResults.push(custom_roles_checks); } + + // check organization actions settings + if (this.policy.actions) { + const actions_checks = await new OrgActionsChecks( + this.policy, + this.organization, + ).evaluate(); + logger.debug(`Org Actions results: ${JSON.stringify(actions_checks)}`); + this.orgCheckResults.push(actions_checks); + } } public printCheckResults() { diff --git a/src/evaluators/organization/OrgActionsChecks.ts b/src/evaluators/organization/OrgActionsChecks.ts new file mode 100644 index 0000000..ef6dc8f --- /dev/null +++ b/src/evaluators/organization/OrgActionsChecks.ts @@ -0,0 +1,67 @@ +import { Organization, CheckResult } from "../../types/common/main"; +import { getOrgActionsPermissions } from "../../github/Actions"; +import { logger } from "../../utils/logger"; + +export class OrgActionsChecks { + private policy: any; + private organization: Organization; + + constructor(policy: any, organization: Organization) { + this.policy = policy; + this.organization = organization; + } + + public async evaluate(): Promise { + const actionsPermissions = await getOrgActionsPermissions( + this.organization.name, + ); + + const checks = { + enabled_repositories: this.checkEnabledRepositories( + actionsPermissions.enabled_repositories, + ), + allowed_actions: this.checkAllowedActions( + actionsPermissions.allowed_actions, + ), + sha_pinning_required: this.checkShaPinningRequired( + (actionsPermissions as any).sha_pinning_required, + ), + }; + + const name = "Org Actions Checks"; + const pass = Object.values(checks).every((check) => check === true); + const data = { + enabled_repositories_github: actionsPermissions.enabled_repositories, + enabled_repositories_policy: this.policy.actions.enabled_repositories, + allowed_actions_github: actionsPermissions.allowed_actions, + allowed_actions_policy: this.policy.actions.allowed_actions, + sha_pinning_required_github: (actionsPermissions as any) + .sha_pinning_required, + sha_pinning_required_policy: this.policy.actions.sha_pinning_required, + ...checks, + }; + + return { name, pass, data }; + } + + private checkEnabledRepositories(githubValue: string | undefined): boolean { + if (this.policy.actions.enabled_repositories === undefined) { + return true; + } + return githubValue === this.policy.actions.enabled_repositories; + } + + private checkAllowedActions(githubValue: string | undefined): boolean { + if (this.policy.actions.allowed_actions === undefined) { + return true; + } + return githubValue === this.policy.actions.allowed_actions; + } + + private checkShaPinningRequired(githubValue: boolean | undefined): boolean { + if (this.policy.actions.sha_pinning_required === undefined) { + return true; + } + return githubValue === this.policy.actions.sha_pinning_required; + } +} diff --git a/src/evaluators/repository/ActionsChecks.ts b/src/evaluators/repository/ActionsChecks.ts index d6e4e95..78f96d7 100644 --- a/src/evaluators/repository/ActionsChecks.ts +++ b/src/evaluators/repository/ActionsChecks.ts @@ -25,12 +25,17 @@ export class ActionsChecks { const actionsPermissionsResult = actionsPermissions.enabled; const actionsPermissionsAllowedActions = actionsPermissions.allowed_actions; const actionsPermissionsPolicy = this.policy.allowed_actions.permission; + const shaPinningRequired = (actionsPermissions as any).sha_pinning_required; + const shaPinningRequiredPolicy = + this.policy.allowed_actions.sha_pinning_required; if (!actionsPermissionsResult) { return this.createResult( actionsPermissionsPolicy === "none", "none", actionsPermissionsPolicy, + shaPinningRequired, + shaPinningRequiredPolicy, ); } @@ -44,6 +49,8 @@ export class ActionsChecks { false, actionsPermissionsAllowedActions, actionsPermissionsPolicy, + shaPinningRequired, + shaPinningRequiredPolicy, ); } if (actionsPermissionsAllowedActions !== "selected") @@ -51,6 +58,8 @@ export class ActionsChecks { false, actionsPermissionsAllowedActions, actionsPermissionsPolicy, + shaPinningRequired, + shaPinningRequiredPolicy, ); const selectedActions = await getRepoSelectedActions( @@ -81,6 +90,8 @@ export class ActionsChecks { verifiedAllowedMatchesPolicy, patternsAllowedActions, this.policy.allowed_actions.selected.patterns_allowed, + shaPinningRequired, + shaPinningRequiredPolicy, ); case "all": case "local_only": @@ -89,6 +100,8 @@ export class ActionsChecks { actionsPermissionsPolicy === actionsPermissionsAllowedActions, actionsPermissionsAllowedActions, actionsPermissionsPolicy, + shaPinningRequired, + shaPinningRequiredPolicy, ); default: logger.error( @@ -101,23 +114,34 @@ export class ActionsChecks { actions_permissions: boolean, github_allowed_actions?: string, policy_allowed_actions?: string, + sha_pinning_required?: boolean, + sha_pinning_required_policy?: boolean, ): CheckResult { let name = "Actions Check"; let pass = false; let data = {}; - if (actions_permissions) { + // Check sha_pinning_required if it's defined in the policy + const shaPinningMatches = + sha_pinning_required_policy === undefined || + sha_pinning_required === sha_pinning_required_policy; + + if (actions_permissions && shaPinningMatches) { pass = true; data = { actions_permissions, github_allowed_actions, policy_allowed_actions, + sha_pinning_required, + sha_pinning_required_policy, }; } else { data = { actions_permissions, github_allowed_actions, policy_allowed_actions, + sha_pinning_required, + sha_pinning_required_policy, }; } @@ -131,12 +155,24 @@ export class ActionsChecks { verified_allowed: boolean, patterns_allowed_github: string[], patterns_allowed_policy: string[], + sha_pinning_required?: boolean, + sha_pinning_required_policy?: boolean, ) { let name = "Actions Check"; let pass = false; let data = {}; - if (actions_permissions && github_owned_allowed && verified_allowed) { + // Check sha_pinning_required if it's defined in the policy + const shaPinningMatches = + sha_pinning_required_policy === undefined || + sha_pinning_required === sha_pinning_required_policy; + + if ( + actions_permissions && + github_owned_allowed && + verified_allowed && + shaPinningMatches + ) { pass = true; data = { actions_permissions, @@ -145,6 +181,8 @@ export class ActionsChecks { verified_allowed, patterns_allowed_github, patterns_allowed_policy, + sha_pinning_required, + sha_pinning_required_policy, }; } else { data = { @@ -154,6 +192,8 @@ export class ActionsChecks { verified_allowed, patterns_allowed_github, patterns_allowed_policy, + sha_pinning_required, + sha_pinning_required_policy, }; } diff --git a/src/github/Actions.ts b/src/github/Actions.ts index df66852..762fdd9 100644 --- a/src/github/Actions.ts +++ b/src/github/Actions.ts @@ -2,6 +2,27 @@ import { Endpoints } from "@octokit/types"; import { GitArmorKit } from "./GitArmorKit"; import { logger } from "../utils/logger"; +//Get GitHub Actions permissions for an organization +export const getOrgActionsPermissions = async ( + org: string, +): Promise< + Endpoints["GET /orgs/{org}/actions/permissions"]["response"]["data"] +> => { + try { + const octokit = new GitArmorKit(); + + const response: Endpoints["GET /orgs/{org}/actions/permissions"]["response"] = + await octokit.rest.actions.getGithubActionsPermissionsOrganization({ + org: org, + }); + + return response.data; + } catch (error) { + logger.error(error.message); + throw error; + } +}; + //Get GitHub Actions permissions for a repository export const getRepoActionsPermissions = async ( owner: string, diff --git a/src/types/common/main.d.ts b/src/types/common/main.d.ts index ba63406..02d474f 100644 --- a/src/types/common/main.d.ts +++ b/src/types/common/main.d.ts @@ -79,6 +79,7 @@ interface AllowedActions { verified_allowed: boolean; patterns_allowed: string[]; }; + sha_pinning_required?: boolean; } interface Workflows { @@ -168,6 +169,11 @@ interface OrgPolicy { member_privileges: MemberPrivileges; authentication: Authentication; advanced_security: AdvancedSecurity; + actions?: { + enabled_repositories?: string; + allowed_actions?: string; + sha_pinning_required?: boolean; + }; } // Generic Check Result From 603c4c5829619c096892b6f788086a79dd3fd3eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 07:39:06 +0000 Subject: [PATCH 3/3] Rename allowed_actions to actions in repository policy for consistency Co-authored-by: david3107 <20040740+david3107@users.noreply.github.com> --- dist/evaluators/RepoPolicyEvaluator.js | 2 +- dist/evaluators/repository/ActionsChecks.js | 14 +++++++------- dist/index.js | 16 ++++++++-------- policies/repository.readme.md | 2 +- policies/repository.yml | 2 +- src/evaluators/RepoPolicyEvaluator.ts | 2 +- src/evaluators/repository/ActionsChecks.ts | 17 +++++++---------- src/types/common/main.d.ts | 4 ++-- 8 files changed, 28 insertions(+), 31 deletions(-) diff --git a/dist/evaluators/RepoPolicyEvaluator.js b/dist/evaluators/RepoPolicyEvaluator.js index 7faf8c2..07d7618 100644 --- a/dist/evaluators/RepoPolicyEvaluator.js +++ b/dist/evaluators/RepoPolicyEvaluator.js @@ -54,7 +54,7 @@ class RepoPolicyEvaluator { this.repositoryCheckResults.push(ghas_checks); } //Run Actions checks - if (this.policy.allowed_actions) { + if (this.policy.actions) { const actions_checks = await new ActionsChecks_1.ActionsChecks(this.policy, this.repository).checkActionsPermissions(); logger_1.logger.debug(`Action checks results: ${JSON.stringify(actions_checks)}`); this.repositoryCheckResults.push(actions_checks); diff --git a/dist/evaluators/repository/ActionsChecks.js b/dist/evaluators/repository/ActionsChecks.js index 5d51fa8..c72eee3 100644 --- a/dist/evaluators/repository/ActionsChecks.js +++ b/dist/evaluators/repository/ActionsChecks.js @@ -15,15 +15,15 @@ class ActionsChecks { const actionsPermissions = await (0, Actions_1.getRepoActionsPermissions)(this.repository.owner, this.repository.name); const actionsPermissionsResult = actionsPermissions.enabled; const actionsPermissionsAllowedActions = actionsPermissions.allowed_actions; - const actionsPermissionsPolicy = this.policy.allowed_actions.permission; + const actionsPermissionsPolicy = this.policy.actions.permission; const shaPinningRequired = actionsPermissions.sha_pinning_required; - const shaPinningRequiredPolicy = this.policy.allowed_actions.sha_pinning_required; + const shaPinningRequiredPolicy = this.policy.actions.sha_pinning_required; if (!actionsPermissionsResult) { return this.createResult(actionsPermissionsPolicy === "none", "none", actionsPermissionsPolicy, shaPinningRequired, shaPinningRequiredPolicy); } switch (actionsPermissionsPolicy) { case "selected": - if (!this.policy.allowed_actions.selected.patterns_allowed) { + if (!this.policy.actions.selected.patterns_allowed) { logger_1.logger.error("error: the policy (.yml) should have the list of patterns_allowed when permission is 'selected'"); return this.createResult(false, actionsPermissionsAllowedActions, actionsPermissionsPolicy, shaPinningRequired, shaPinningRequiredPolicy); } @@ -33,12 +33,12 @@ class ActionsChecks { const githubOwnedAllowedActions = selectedActions.github_owned_allowed; const verifiedAllowedActions = selectedActions.verified_allowed; const patternsAllowedActions = selectedActions.patterns_allowed; - const selectedActionsAllowed = selectedActions.patterns_allowed.every((action) => this.policy.allowed_actions.selected.patterns_allowed.includes(action)); - const githubOwnedAllowedMatchesPolicy = this.policy.allowed_actions.selected.github_owned_allowed === + const selectedActionsAllowed = selectedActions.patterns_allowed.every((action) => this.policy.actions.selected.patterns_allowed.includes(action)); + const githubOwnedAllowedMatchesPolicy = this.policy.actions.selected.github_owned_allowed === githubOwnedAllowedActions; - const verifiedAllowedMatchesPolicy = this.policy.allowed_actions.selected.verified_allowed === + const verifiedAllowedMatchesPolicy = this.policy.actions.selected.verified_allowed === verifiedAllowedActions; - return this.createResultSelected(selectedActionsAllowed, actionsPermissionsAllowedActions, githubOwnedAllowedMatchesPolicy, verifiedAllowedMatchesPolicy, patternsAllowedActions, this.policy.allowed_actions.selected.patterns_allowed, shaPinningRequired, shaPinningRequiredPolicy); + return this.createResultSelected(selectedActionsAllowed, actionsPermissionsAllowedActions, githubOwnedAllowedMatchesPolicy, verifiedAllowedMatchesPolicy, patternsAllowedActions, this.policy.actions.selected.patterns_allowed, shaPinningRequired, shaPinningRequiredPolicy); case "all": case "local_only": case "none": diff --git a/dist/index.js b/dist/index.js index 670e11b..676da3b 100644 --- a/dist/index.js +++ b/dist/index.js @@ -47929,7 +47929,7 @@ class RepoPolicyEvaluator { this.repositoryCheckResults.push(ghas_checks); } //Run Actions checks - if (this.policy.allowed_actions) { + if (this.policy.actions) { const actions_checks = await new ActionsChecks_1.ActionsChecks(this.policy, this.repository).checkActionsPermissions(); logger_1.logger.debug(`Action checks results: ${JSON.stringify(actions_checks)}`); this.repositoryCheckResults.push(actions_checks); @@ -48384,15 +48384,15 @@ class ActionsChecks { const actionsPermissions = await (0, Actions_1.getRepoActionsPermissions)(this.repository.owner, this.repository.name); const actionsPermissionsResult = actionsPermissions.enabled; const actionsPermissionsAllowedActions = actionsPermissions.allowed_actions; - const actionsPermissionsPolicy = this.policy.allowed_actions.permission; + const actionsPermissionsPolicy = this.policy.actions.permission; const shaPinningRequired = actionsPermissions.sha_pinning_required; - const shaPinningRequiredPolicy = this.policy.allowed_actions.sha_pinning_required; + const shaPinningRequiredPolicy = this.policy.actions.sha_pinning_required; if (!actionsPermissionsResult) { return this.createResult(actionsPermissionsPolicy === "none", "none", actionsPermissionsPolicy, shaPinningRequired, shaPinningRequiredPolicy); } switch (actionsPermissionsPolicy) { case "selected": - if (!this.policy.allowed_actions.selected.patterns_allowed) { + if (!this.policy.actions.selected.patterns_allowed) { logger_1.logger.error("error: the policy (.yml) should have the list of patterns_allowed when permission is 'selected'"); return this.createResult(false, actionsPermissionsAllowedActions, actionsPermissionsPolicy, shaPinningRequired, shaPinningRequiredPolicy); } @@ -48402,12 +48402,12 @@ class ActionsChecks { const githubOwnedAllowedActions = selectedActions.github_owned_allowed; const verifiedAllowedActions = selectedActions.verified_allowed; const patternsAllowedActions = selectedActions.patterns_allowed; - const selectedActionsAllowed = selectedActions.patterns_allowed.every((action) => this.policy.allowed_actions.selected.patterns_allowed.includes(action)); - const githubOwnedAllowedMatchesPolicy = this.policy.allowed_actions.selected.github_owned_allowed === + const selectedActionsAllowed = selectedActions.patterns_allowed.every((action) => this.policy.actions.selected.patterns_allowed.includes(action)); + const githubOwnedAllowedMatchesPolicy = this.policy.actions.selected.github_owned_allowed === githubOwnedAllowedActions; - const verifiedAllowedMatchesPolicy = this.policy.allowed_actions.selected.verified_allowed === + const verifiedAllowedMatchesPolicy = this.policy.actions.selected.verified_allowed === verifiedAllowedActions; - return this.createResultSelected(selectedActionsAllowed, actionsPermissionsAllowedActions, githubOwnedAllowedMatchesPolicy, verifiedAllowedMatchesPolicy, patternsAllowedActions, this.policy.allowed_actions.selected.patterns_allowed, shaPinningRequired, shaPinningRequiredPolicy); + return this.createResultSelected(selectedActionsAllowed, actionsPermissionsAllowedActions, githubOwnedAllowedMatchesPolicy, verifiedAllowedMatchesPolicy, patternsAllowedActions, this.policy.actions.selected.patterns_allowed, shaPinningRequired, shaPinningRequiredPolicy); case "all": case "local_only": case "none": diff --git a/policies/repository.readme.md b/policies/repository.readme.md index 3c7c20a..c0d9afa 100644 --- a/policies/repository.readme.md +++ b/policies/repository.readme.md @@ -67,7 +67,7 @@ protected_branches: The policy checks the actions permissions for the specified repository. ```yml -allowed_actions: +actions: permission: local_only selected: github_owned_allowed: true diff --git a/policies/repository.yml b/policies/repository.yml index 30cb334..d92c16a 100644 --- a/policies/repository.yml +++ b/policies/repository.yml @@ -51,7 +51,7 @@ advanced_security: dependabot_version_updates: true code_scanning: true -allowed_actions: +actions: permission: selected # all, local_only, selected, none selected: github_owned_allowed: true diff --git a/src/evaluators/RepoPolicyEvaluator.ts b/src/evaluators/RepoPolicyEvaluator.ts index ba852bf..785c7d6 100644 --- a/src/evaluators/RepoPolicyEvaluator.ts +++ b/src/evaluators/RepoPolicyEvaluator.ts @@ -89,7 +89,7 @@ export class RepoPolicyEvaluator { } //Run Actions checks - if (this.policy.allowed_actions) { + if (this.policy.actions) { const actions_checks = await new ActionsChecks( this.policy, this.repository, diff --git a/src/evaluators/repository/ActionsChecks.ts b/src/evaluators/repository/ActionsChecks.ts index 78f96d7..523071b 100644 --- a/src/evaluators/repository/ActionsChecks.ts +++ b/src/evaluators/repository/ActionsChecks.ts @@ -24,10 +24,9 @@ export class ActionsChecks { const actionsPermissionsResult = actionsPermissions.enabled; const actionsPermissionsAllowedActions = actionsPermissions.allowed_actions; - const actionsPermissionsPolicy = this.policy.allowed_actions.permission; + const actionsPermissionsPolicy = this.policy.actions.permission; const shaPinningRequired = (actionsPermissions as any).sha_pinning_required; - const shaPinningRequiredPolicy = - this.policy.allowed_actions.sha_pinning_required; + const shaPinningRequiredPolicy = this.policy.actions.sha_pinning_required; if (!actionsPermissionsResult) { return this.createResult( @@ -41,7 +40,7 @@ export class ActionsChecks { switch (actionsPermissionsPolicy) { case "selected": - if (!this.policy.allowed_actions.selected.patterns_allowed) { + if (!this.policy.actions.selected.patterns_allowed) { logger.error( "error: the policy (.yml) should have the list of patterns_allowed when permission is 'selected'", ); @@ -72,15 +71,13 @@ export class ActionsChecks { const selectedActionsAllowed = selectedActions.patterns_allowed.every( (action: string) => - this.policy.allowed_actions.selected.patterns_allowed.includes( - action, - ), + this.policy.actions.selected.patterns_allowed.includes(action), ); const githubOwnedAllowedMatchesPolicy = - this.policy.allowed_actions.selected.github_owned_allowed === + this.policy.actions.selected.github_owned_allowed === githubOwnedAllowedActions; const verifiedAllowedMatchesPolicy = - this.policy.allowed_actions.selected.verified_allowed === + this.policy.actions.selected.verified_allowed === verifiedAllowedActions; return this.createResultSelected( @@ -89,7 +86,7 @@ export class ActionsChecks { githubOwnedAllowedMatchesPolicy, verifiedAllowedMatchesPolicy, patternsAllowedActions, - this.policy.allowed_actions.selected.patterns_allowed, + this.policy.actions.selected.patterns_allowed, shaPinningRequired, shaPinningRequiredPolicy, ); diff --git a/src/types/common/main.d.ts b/src/types/common/main.d.ts index 02d474f..abc56ab 100644 --- a/src/types/common/main.d.ts +++ b/src/types/common/main.d.ts @@ -72,7 +72,7 @@ interface AdvancedSecurity { code_scanning: boolean; } -interface AllowedActions { +interface Actions { permission: string; selected: { github_owned_allowed: boolean; @@ -117,7 +117,7 @@ interface RepoPolicy { protected_branches: ProtectedBranch[]; file_exists: string[]; advanced_security: AdvancedSecurity; - allowed_actions: AllowedActions; + actions: Actions; workflows: Workflows; runners: Runners; webhooks: WebHook;