From 36c654701d50a9e448d4df4b3b5eeab6c8cfc512 Mon Sep 17 00:00:00 2001 From: Yadhav Jayaraman <57544838+decyjphr@users.noreply.github.com> Date: Thu, 11 Nov 2021 18:57:29 -0500 Subject: [PATCH] add compliance report function --- .github/workflows/node-ci.yml | 5 ++- README.md | 5 +++ index.js | 75 ++++++++++++++++++++++++----------- lib/plugins/branches.js | 4 +- lib/plugins/repository.js | 4 +- lib/settings.js | 51 ++++++++++++++++++++++++ 6 files changed, 113 insertions(+), 31 deletions(-) diff --git a/.github/workflows/node-ci.yml b/.github/workflows/node-ci.yml index 1a69a3e3..eec10a52 100644 --- a/.github/workflows/node-ci.yml +++ b/.github/workflows/node-ci.yml @@ -74,12 +74,13 @@ jobs: docker run --env APP_ID=${{ secrets.APP_ID }} --env PRIVATE_KEY=${{ secrets.PRIVATE_KEY }} --env WEBHOOK_SECRET=${{ secrets.WEBHOOK_SECRET }} -d -p 3000:3000 yadhav/safe-settings:main-enterprise sleep 5 curl http://localhost:3000 - - name: Tag a rc release + - name: Tag a release id: rcrelease uses: actionsdesk/semver@0.6.0-rc.8 with: + bump: patch prerelease: withBuildNumber - prelabel: rc + prelabel: alpha - name: Push Docker Image if: ${{ success() }} uses: docker/build-push-action@v2 diff --git a/README.md b/README.md index 6de153f4..52918513 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ 1. `Suborg` level settings. A `suborg` is an arbitrary collection of repos belonging to projects, business units, or teams. The `suborg`settings reside in a yaml file for each `suborg` in the `.github/suborgs`folder. 1. `Repo` level settings. They reside in a repo specific yaml in `.github/repos`folder 1. It is recommended to break the settings into org-level, suborg-level, and repo-level units. This will allow different teams to be define and manage policies for their specific projects or business units.With `CODEOWNERS`, this will allow different people to be responsible for approving changes in different projects. +2. `safe-settings` can create a compliance report of all repositories in the org **Note:** The settings file must have a `.yml`extension only. `.yaml` extension is ignored, for now. @@ -37,6 +38,10 @@ The App can be configured to apply the settings on a schedule. This could a way To set periodically converge the settings to the configuration, set the `CRON` environment variable. This is based on [node-cron](https://www.npmjs.com/package/node-cron) and details on the possible values can be found [here](#Env variables). +### Report + +If you go to /admin/report, it will create a report of all the repositories in the org and what changes would be applied to them. This could be used to identify repositories that are non-compliant. + ### Pull Request Workflow `Safe-settings` explicitly looks in the `admin` repo in the organization for the settings files. The `admin` repo could be a restricted repository with `branch protections` and `codeowners` diff --git a/index.js b/index.js index 6354402b..62bf7dd4 100644 --- a/index.js +++ b/index.js @@ -6,8 +6,21 @@ const Glob = require('./lib/glob') const ConfigManager = require('./lib/configManager') let deploymentConfig -module.exports = (robot, _, Settings = require('./lib/settings')) => { - + +module.exports = (robot, { getRouter }) => { + // Get an express router to expose new HTTP endpoints + const router = getRouter("/admin") + + // Use any middleware + router.use(require("express").static("public")) + + // Add a new route + router.get("/report", async (req, res) => { + const report = await reportInstallation(true) + res.send(report) + }) + + const Settings = require('./lib/settings') async function syncAllSettings (nop, context, repo = context.repo(), ref) { deploymentConfig = await loadYamlFileSystem() robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`) @@ -22,6 +35,16 @@ module.exports = (robot, _, Settings = require('./lib/settings')) => { } } + async function reportAllSettings (nop, context, repo = context.repo()) { + deploymentConfig = await loadYamlFileSystem() + robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`) + const configManager = new ConfigManager(context) + const runtimeConfig = await configManager.loadGlobalSettingsYaml(); + const config = Object.assign({}, deploymentConfig, runtimeConfig) + robot.log.debug(`config is ${JSON.stringify(config)}`) + return await Settings.reportAll(nop, context, repo, config) + } + async function syncSubOrgSettings (nop, context, suborg, repo = context.repo(), ref) { deploymentConfig = await loadYamlFileSystem() robot.log.debug(`deploymentConfig is ${JSON.stringify(deploymentConfig)}`) @@ -204,28 +227,34 @@ module.exports = (robot, _, Settings = require('./lib/settings')) => { robot.log.debug(JSON.stringify(res,null)) } - async function syncInstallation () { - robot.log.trace('Fetching installations') - const github = await robot.auth() + async function reportInstallation(nop = false) { + const context = await getAppInstallationContext() + return await reportAllSettings(nop, context) + } + + async function syncInstallation() { + const context = await getAppInstallationContext() + return syncAllSettings(false, context) + } + async function getAppInstallationContext() { + robot.log.trace('Fetching installations') + let github = await robot.auth() const installations = await github.paginate( github.apps.listInstallations.endpoint.merge({ per_page: 100 }) ) - - for (installation of installations) { - robot.log.trace(`${JSON.stringify(installation)}`) - const github = await robot.auth(installation.id) - const context = { - payload: { - installation: installation - }, - octokit: github, - log: robot.log, - repo: () => { return {repo: "admin", owner: installation.account.login}} - } - return syncAllSettings(false, context) - } - retrun + const installation = installations[0] + robot.log.trace(`${JSON.stringify(installation)}`) + github = await robot.auth(installation.id) + const context = { + payload: { + installation: installation + }, + octokit: github, + log: robot.log, + repo: () => { return {repo: "admin", owner: installation.account.login}} + } + return context } robot.on('push', async context => { @@ -452,10 +481,8 @@ module.exports = (robot, _, Settings = require('./lib/settings')) => { # * * * * * * */ cron.schedule(process.env.CRON, () => { - console.log('running a task every minute'); + robot.log.debug('running a task every minute'); syncInstallation() }); - } - - + } } diff --git a/lib/plugins/branches.js b/lib/plugins/branches.js index dc06c766..75f44519 100644 --- a/lib/plugins/branches.js +++ b/lib/plugins/branches.js @@ -1,6 +1,6 @@ const NopCommand = require('../nopcommand') const MergeDeep = require('../mergeDeep') -const ignorableFields = [] +const ignorableFields = ['enforce_admins'] const previewHeaders = { accept: 'application/vnd.github.hellcat-preview+json,application/vnd.github.luke-cage-preview+json,application/vnd.github.zzzax-preview+json' } module.exports = class Branches { @@ -62,7 +62,7 @@ module.exports = class Branches { const mergeDeep = new MergeDeep(this.log,ignorableFields) const results = JSON.stringify(mergeDeep.compareDeep(result.data, branch.protection),null,2) this.log(`Result of compareDeep = ${results}`) - resArray.push(new NopCommand("Branch Protection", this.repo, null, `Followings changes will be applied to the branch protection for ${params.branch} branch = ${results}`)) + resArray.push(new NopCommand("Branch Protection", this.repo, null, `Branch protection changes \`${params.branch}\` branch = ${results}`)) } catch(e){ this.log.error(e) } diff --git a/lib/plugins/repository.js b/lib/plugins/repository.js index 029d994f..5f3b9cb3 100644 --- a/lib/plugins/repository.js +++ b/lib/plugins/repository.js @@ -1,5 +1,3 @@ -//const { restEndpointMethods } = require('@octokit/plugin-rest-endpoint-methods') -//const EndPoints = require('@octokit/plugin-rest-endpoint-methods') const NopCommand = require('../nopcommand') const MergeDeep = require('../mergeDeep') const ignorableFields = [ @@ -83,7 +81,7 @@ module.exports = class Repository { const mergeDeep = new MergeDeep(this.log,ignorableFields) const results = JSON.stringify(mergeDeep.compareDeep(resp.data, this.settings),null,2) this.log(`Result of compareDeep = ${results}`) - resArray.push(new NopCommand("Repository", this.repo, null, `Followings changes will be applied to the repo settings = ${results}`)) + resArray.push(new NopCommand("Repository", this.repo, null, `Repository changes = ${results}`)) } catch(e){ this.log.error(e) } diff --git a/lib/settings.js b/lib/settings.js index a667af87..7b3ca8f2 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -11,6 +11,13 @@ class Settings { await settings.handleResults() } + static async reportAll(nop, context, repo, config) { + const settings = new Settings(nop, context, repo, config) + await settings.loadConfigs() + await settings.updateAll() + return await settings.reportResults() + } + static async sync(nop, context, repo, config, ref) { const { payload } = context const settings = new Settings(nop, context, repo, config, ref) @@ -31,6 +38,50 @@ class Settings { this.results = [] } + async reportResults() { + if (!this.nop) { + this.log.debug(`Not run in nop`) + return + } + + this.results.sort((s,t) => { + if (s.repo < t.repo) return -1 + if (s.repo > t.repo) return 1 + return 0 + }) + + let error = false; + + const commentmessage = ` +

🤖 Safe-Settings Repository Report

+${this.results.reduce((x,y) => { + if (!y) { + return x + } + if (y.endpoint) { + // ignore this + return x + } else { + if (y.type === "ERROR") { + error = true + return `${x} +
+ ❌ ${y.action} +
` + } else { + return `${x} +

+ - ℹī¸ ${y.repo} : ${y.action} +

` + + } + } + +}, '')} +` + return commentmessage + } + async handleResults() { const { payload } = this.context if (!this.nop) {