Skip to content

Add compliance report function #152

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

Draft
wants to merge 1 commit into
base: main-enterprise
Choose a base branch
from
Draft
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
@@ -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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 <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`

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


}
}
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 {
@@ -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)
}
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 = [
@@ -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)
}
51 changes: 51 additions & 0 deletions lib/settings.js
Original file line number Diff line number Diff line change
@@ -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 = `
<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) {