Repository navigation
Expand file tree
/
Copy pathdependabot-security.mjs
More file actions
179 lines (165 loc) · 6.51 KB
/
Copy pathdependabot-security.mjs
File metadata and controls
179 lines (165 loc) · 6.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
#!/usr/bin/env node
// Copyright (c) 2026 Sub Rosa contributors
import { createLogger } from '../packages/logging/src/index.cjs';
const diagnostics = createLogger("scripts.dependabot-security");
/**
* Check (and optionally enable) the repository-level Dependabot security
* settings described in docs/DEPENDABOT_SECURITY.md:
*
* 1. Dependency graph (always on for public repositories)
* 2. Dependabot alerts PUT /repos/{repo}/vulnerability-alerts
* 3. Dependabot security updates PUT /repos/{repo}/automated-security-fixes
*
* These are repository settings, not files, so they cannot ship in a PR. The
* script lets a maintainer with admin access apply them and lets anyone with
* admin access verify them later.
*
* Usage:
* node scripts/dependabot-security.mjs [--repo owner/name] # check
* node scripts/dependabot-security.mjs [--repo owner/name] --apply # enable
*
* Exit codes: 0 all settings enabled, 1 something is disabled or unknown,
* 2 bad usage. Scheduled version updates live in .github/dependabot.yml (#482)
* and are intentionally out of scope here.
*/
import { execFileSync } from "node:child_process";
import { pathToFileURL } from "node:url";
export const DEFAULT_REPO = "Sub-Rosa-Issue/sub-rosa-issue";
/**
* Llama a `gh api` y devuelve `{ ok, status, body }` sin tirar.
* gh sale con codigo distinto de cero ante cualquier 4xx, asi que el status
* se saca del texto de error ("HTTP 404").
*/
export function ghApi(method, path) {
try {
const body = execFileSync("gh", ["api", "--method", method, "-H", "Accept: application/vnd.github+json", path], {
encoding: "utf-8",
stdio: ["ignore", "pipe", "pipe"],
});
return { ok: true, status: body.trim() ? 200 : 204, body };
} catch (error) {
const stderr = String(error.stderr ?? "");
const match = stderr.match(/HTTP (\d{3})/);
return { ok: false, status: match ? Number(match[1]) : null, body: stderr.trim() };
}
}
/** Lee el estado actual de las tres configuraciones. */
export function readState(repo, api = ghApi) {
const meta = api("GET", `repos/${repo}`);
let visibility = null;
if (meta.ok) {
try {
visibility = JSON.parse(meta.body).visibility ?? null;
} catch {
visibility = null;
}
}
// 204 = activado, 404 = desactivado (o sin permiso de admin: GitHub no distingue).
const alerts = api("GET", `repos/${repo}/vulnerability-alerts`);
const alertsEnabled = alerts.ok ? true : alerts.status === 404 ? false : null;
// 200 con { enabled, paused }; 404 cuando las alertas estan apagadas.
const fixes = api("GET", `repos/${repo}/automated-security-fixes`);
let securityUpdatesEnabled = null;
let securityUpdatesPaused = false;
if (fixes.ok) {
try {
const parsed = JSON.parse(fixes.body);
securityUpdatesEnabled = parsed.enabled === true;
securityUpdatesPaused = parsed.paused === true;
} catch {
securityUpdatesEnabled = null;
}
} else if (fixes.status === 404) {
securityUpdatesEnabled = false;
}
return {
visibility,
// El grafo de dependencias no se puede apagar en repos publicos.
dependencyGraph: visibility === "public" ? true : null,
alerts: alertsEnabled,
securityUpdates: securityUpdatesEnabled,
securityUpdatesPaused,
};
}
/** Convierte un estado en filas legibles y un veredicto. */
export function evaluate(state) {
const label = (value) => (value === true ? "enabled" : value === false ? "disabled" : "unknown");
const rows = [
{
id: "dependency-graph",
name: "Dependency graph",
status: label(state.dependencyGraph),
note:
state.dependencyGraph === null
? "Not a public repo (or metadata unreadable): enable it under Settings → Code security."
: "Always on for public repositories.",
},
{
id: "dependabot-alerts",
name: "Dependabot alerts",
status: label(state.alerts),
note: state.alerts === true ? "" : "Requires admin access to read or change.",
},
{
id: "dependabot-security-updates",
name: "Dependabot security updates",
status: state.securityUpdatesPaused ? "paused" : label(state.securityUpdates),
note: state.securityUpdatesPaused ? "GitHub paused security updates; re-enable in Settings → Code security." : "",
},
];
const ok = rows.every((row) => row.status === "enabled");
return { ok, rows };
}
/** Activa alertas y luego actualizaciones de seguridad (el orden importa). */
export function apply(repo, api = ghApi) {
const steps = [
["dependabot-alerts", `repos/${repo}/vulnerability-alerts`],
["dependabot-security-updates", `repos/${repo}/automated-security-fixes`],
];
const results = [];
for (const [id, path] of steps) {
const res = api("PUT", path);
results.push({ id, ok: res.ok, status: res.status, error: res.ok ? null : res.body });
// Sin alertas GitHub rechaza las actualizaciones de seguridad: no seguir.
if (!res.ok) break;
}
return results;
}
export function parseArgs(argv) {
const opts = { repo: DEFAULT_REPO, apply: false };
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (arg === "--apply") opts.apply = true;
else if (arg === "--repo") {
const value = argv[++i];
if (!value || !/^[\w.-]+\/[\w.-]+$/.test(value)) throw new Error(`--repo expects owner/name, got: ${value ?? "(nothing)"}`);
opts.repo = value;
} else throw new Error(`Unknown argument: ${arg}`);
}
return opts;
}
export function main(argv = process.argv.slice(2), api = ghApi) {
let opts;
try {
opts = parseArgs(argv);
} catch (error) {
diagnostics.error("usage", error.message);
return 2;
}
if (opts.apply) {
for (const step of apply(opts.repo, api)) {
if (step.ok) diagnostics.info("applied", `Enabled ${step.id} on ${opts.repo}`);
else diagnostics.error("apply-failed", `Could not enable ${step.id} on ${opts.repo} (HTTP ${step.status ?? "?"})`, { error: step.error });
}
}
const { ok, rows } = evaluate(readState(opts.repo, api));
for (const row of rows) {
diagnostics.info("setting", `[${row.status.toUpperCase()}] ${row.name}${row.note ? ` — ${row.note}` : ""}`, { id: row.id });
}
if (ok) diagnostics.info("passed", `All Dependabot security settings are enabled on ${opts.repo}`);
else diagnostics.error("failed", `Dependabot security settings are incomplete on ${opts.repo}; see docs/DEPENDABOT_SECURITY.md`);
return ok ? 0 : 1;
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
process.exitCode = main();
}