Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add compliance report function #152

Draft
wants to merge 1 commit into
base: main-enterprise
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/node-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/[email protected]
with:
bump: patch
prerelease: withBuildNumber
prelabel: rc
prelabel: alpha
- name: Push Docker Image
if: ${{ success() }}
uses: docker/build-push-action@v2
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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 <safe-settings-url>/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`

Expand Down
75 changes: 51 additions & 24 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)}`)
Expand All @@ -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)}`)
Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -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()
});
}


}
}
4 changes: 2 additions & 2 deletions lib/plugins/branches.js
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down
4 changes: 1 addition & 3 deletions lib/plugins/repository.js
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down Expand Up @@ -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)
}
Expand Down
51 changes: 51 additions & 0 deletions lib/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 = `
<h1> 🤖 Safe-Settings Repository Report </h1>
${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}
<details>
<summary>❌ ${y.action}</summary>
</details>`
} else {
return `${x}
<p>
- ℹ️ ${y.repo} : ${y.action}
</p>`

}
}

}, '')}
`
return commentmessage
}

async handleResults() {
const { payload } = this.context
if (!this.nop) {
Expand Down