Desde el mes de Abril de este año, tienes en public beta la posibilidad de crear deployment protection rules para tus flujos de GitHub Actions, lo cual significa que podrás generar tus propias reglas (o usar algunas de las generadas por otras compañías) antes de pasar de un entorno a otro, haciendo las veces de gates o checks. En mi vídeo sobre Controlar el paso entre entornos con aprobaciones y security gates, de mi serie sobre DevSecOps, te mostré cómo podías usar estas reglas desde un punto de vista de seguridad.
Hoy quiero compartir contigo el código de esa demo, un poquito actualizado 😃, donde compruebo antes de hacer el paso a producción que no tengo ninguna alerta en GitHub Advanced Security del tipo code scanning.
¿Qué necesitas?
Para poder generar este tipo de reglas necesitas:
- Una GitHub App con los permisos necesarios, además de estar suscrita al evento llamado Deployment protection rule, tal y como te explico a partir de este punto del vídeo.
- Un webhook, desplegado en algún sitio, al que puedas llamar cuando ese evento se produce. En mi ejemplo utilizo Ngrok para poder llamar a mi webhook ejecutándose en local.
- Definir los environments en tu repositorio, a los que asociarle estas reglas.
Ejemplo de deployment protection rule
Una vez que ya tienes tu GitHub App, para que veas el tipo de cosas que podrías hacer con estas reglas, este podría ser un ejemplo de cómo aprobar o denegar una petición de paso al entorno de desarrollo y al de producción:
require('dotenv').config();
const express = require('express'),
{ App } = require("octokit"),
fs = require('fs');
const gh_app = new App({
appId: process.env.GH_APP_ID,
privateKey: fs.readFileSync("private-key.pem"),
});
const PORT = process.env.PORT || 3000;
const app = express();
app.use(express.json());
app.post('/hook', async (req, res) => {
let action = req.body.action,
environment = req.body.environment,
owner = req.body.repository.owner.login,
repo = req.body.repository.name,
deployment_callback_url = req.body.deployment_callback_url,
runId = deployment_callback_url.match(/runs\/(\d+)\//)[1],
installationId = req.body.installation.id,
deploymentBranch = req.body.deployment.ref;
console.log(`Action: ${action}`);
console.log(`Environment: ${environment}`);
console.log(`Owner: ${owner}`);
console.log(`Repository: ${repo}`);
console.log(`Deployment callback URL: ${deployment_callback_url}`);
console.log(`Run ID: ${runId}`);
console.log(`Installation ID: ${installationId}`);
console.log(`Deployment branch: ${deploymentBranch}`);
const octokit = await gh_app.getInstallationOctokit(installationId);
let response = await octokit.request(`GET /repos/{owner}/{repo}/code-scanning/alerts`, {
owner: owner,
repo: repo,
headers: {
'X-GitHub-Api-Version': '2022-11-28'
}
});
let alerts = response.data;
// Check if some of the alerts are high and open
let highAlerts = alerts.filter(alert => (alert.rule.severity === 'high' || alert.rule.severity === 'error') && alert.state === 'open');
console.log(`Number of alerts: ${alerts.length}`);
console.log(`Number of high alerts open: ${highAlerts.length}`);
let status = 'approved';
let message = '';
switch (environment) {
case 'dev':
message = `There are ${highAlerts.length} high alerts in the ${environment} environment. But we are going to deploy anyway.`;
break;
case 'prod':
// Check if high alerts are in main branch
let highAlertsInMain = highAlerts.filter(alert => alert.most_recent_instance.ref === 'refs/heads/main');
if (highAlertsInMain.length > 0) {
message = `There are ${highAlertsInMain.length} high alerts in the ${environment} environment in main branch. Deployment is rejected.`;
status = 'rejected';
}
else {
message = `Good news! There are no high alerts in the ${environment} environment in main branch. Deployment is approved.`;
}
break;
}
// Create a deployment status
response = await octokit.request('POST /repos/{owner}/{repo}/actions/runs/{run_id}/deployment_protection_rule', {
owner: owner,
repo: repo,
run_id: runId,
environment_name: environment,
state: status,
comment: message,
headers: {
'X-GitHub-Api-Version': '2022-11-28'
}
});
console.log(`Response from the deployment callback URL: ${response.status}`);
res.status(200).send('OK');
});
app.listen(PORT, () => {
console.log(`🚀 Server listening on port ${PORT}`);
});
Para poder probar todo esto, en este mismo repositorio he creado un flujo de GitHub Actions que hace uso de los environments que he definido en mi repo para poder comprobar que esta regla funciona como espero:
on:
push:
branches: [main]
jobs:
test_gate_in_dev:
name: Test Gate in Dev
runs-on: ubuntu-latest
environment:
name: dev
steps:
- uses: actions/checkout@v4
- name: Approved
run: |
echo Gate passed! 👍🏻
test_gate_in_prod:
name: Test Gate in Prod
runs-on: ubuntu-latest
needs: [test_gate_in_dev]
environment:
name: prod
steps:
- uses: actions/checkout@v4
- name: Approved
run: |
echo Gate passed! 👍🏻
¡Saludos!