Skip to content
Merged
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
137 changes: 136 additions & 1 deletion action/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,7 @@ function diffRepos(desired, live, opts, out) {
}
diffBranchProtection(name, dr.branchProtection, lr.branchProtection ?? [], opts, out);
diffRulesets(`${name}/`, "repo-ruleset", dr.rulesets, lr.rulesets ?? [], opts, out);
diffRepoSecurity(name, dr.security, lr.security, out);
}
for (const name of Object.keys(live)) {
if (!Object.prototype.hasOwnProperty.call(desired, name)) {
Expand Down Expand Up @@ -584,6 +585,24 @@ function diffRulesets(keyPrefix, resourceType, desired, live, opts, out) {
}
}
}
function diffRepoSecurity(repoName, desired, live, out) {
if (desired === void 0) return;
if (live === void 0) {
out.push({ kind: "create", resourceType: "repo-security", key: repoName, after: desired });
return;
}
const fields = diffObject(desired, live);
if (fields.length > 0) {
out.push({
kind: "update",
resourceType: "repo-security",
key: repoName,
before: live,
after: desired,
fields
});
}
}
function diffObject(desired, live) {
const fields = [];
for (const key of Object.keys(desired)) {
Expand Down Expand Up @@ -659,6 +678,7 @@ var init_diff = __esm({
"team-repo",
"member",
"repo",
"repo-security",
"branch-protection",
"repo-ruleset"
];
Expand Down Expand Up @@ -229948,14 +229968,129 @@ var rulesetsCycle = {
}
};

// src/cycles/security-features.ts
function hasManagedSecurity(repo) {
return repo.security !== void 0;
}
function statusToBool(s) {
if (s == null || s.status == null) return void 0;
return s.status === "enabled";
}
async function fetchRepoSecurity(client, org, repo, budget) {
const live = {};
budget.use(1);
const repoData = await client.request("GET", `/repos/${org}/${repo}`);
const saa = repoData.security_and_analysis ?? {};
const adv = statusToBool(saa.advanced_security);
const ss = statusToBool(saa.secret_scanning);
const ssp = statusToBool(saa.secret_scanning_push_protection);
if (adv !== void 0) live.advancedSecurity = adv;
if (ss !== void 0) live.secretScanning = ss;
if (ssp !== void 0) live.secretScanningPushProtection = ssp;
if (!budget.exhausted) {
budget.use(1);
try {
await client.request("GET", `/repos/${org}/${repo}/vulnerability-alerts`);
live.vulnerabilityAlerts = true;
} catch (err) {
if (err instanceof Error && err.message.includes("404")) {
live.vulnerabilityAlerts = false;
} else {
throw err;
}
}
}
if (!budget.exhausted) {
budget.use(1);
try {
const fixes = await client.request(
"GET",
`/repos/${org}/${repo}/automated-security-fixes`
);
live.dependabotSecurityUpdates = fixes.enabled === true;
} catch (err) {
if (err instanceof Error && err.message.includes("404")) {
live.dependabotSecurityUpdates = false;
} else {
throw err;
}
}
}
return live;
}
function buildSecurityAnalysisBody(desired) {
const saa = {};
if (desired.advancedSecurity !== void 0) {
saa.advanced_security = { status: desired.advancedSecurity ? "enabled" : "disabled" };
}
if (desired.secretScanning !== void 0) {
saa.secret_scanning = { status: desired.secretScanning ? "enabled" : "disabled" };
}
if (desired.secretScanningPushProtection !== void 0) {
saa.secret_scanning_push_protection = {
status: desired.secretScanningPushProtection ? "enabled" : "disabled"
};
}
return saa;
}
var securityFeaturesCycle = {
name: "security-features",
// ── Part 2: fetchLive ──────────────────────────────────────────────────────
async fetchLive(client, orgLogin, scope, budget) {
if (budget.exhausted) {
const { BudgetExhaustedError: BudgetExhaustedError2 } = await Promise.resolve().then(() => (init_runner(), runner_exports));
throw new BudgetExhaustedError2();
}
const repos = {};
for (const [name, repoConfig] of Object.entries(scope?.repos ?? {})) {
if (!hasManagedSecurity(repoConfig)) continue;
if (budget.exhausted) break;
repos[name] = { security: await fetchRepoSecurity(client, orgLogin, name, budget) };
}
return { repos };
},
// ── Part 3: buildDesired ───────────────────────────────────────────────────
buildDesired(orgConfig, _orgLogin, _scope) {
if (!orgConfig.repos) return {};
const repos = {};
for (const [name, repoConfig] of Object.entries(orgConfig.repos)) {
if (hasManagedSecurity(repoConfig)) repos[name] = { security: repoConfig.security };
}
return { repos };
},
// ── Part 4: apply ──────────────────────────────────────────────────────────
async apply(client, entry, orgLogin, _scope, budget) {
if (entry.resourceType !== "repo-security") return;
if (entry.kind === "delete") return;
const repo = entry.key;
const desired = entry.after;
const saa = buildSecurityAnalysisBody(desired);
if (Object.keys(saa).length > 0) {
budget.use(1);
await client.request("PATCH", `/repos/${orgLogin}/${repo}`, { security_and_analysis: saa });
}
if (desired.vulnerabilityAlerts !== void 0) {
budget.use(1);
const method = desired.vulnerabilityAlerts ? "PUT" : "DELETE";
await client.request(method, `/repos/${orgLogin}/${repo}/vulnerability-alerts`);
}
if (desired.dependabotSecurityUpdates !== void 0) {
budget.use(1);
const method = desired.dependabotSecurityUpdates ? "PUT" : "DELETE";
await client.request(method, `/repos/${orgLogin}/${repo}/automated-security-fixes`);
}
}
};

// src/cli/registry.ts
var CYCLE_REGISTRY = {
[branchProtectionCycle.name]: branchProtectionCycle,
[orgSettingsCycle.name]: orgSettingsCycle,
[repoSettingsCycle.name]: repoSettingsCycle,
[membershipCycle.name]: membershipCycle,
[teamsCycle.name]: teamsCycle,
[rulesetsCycle.name]: rulesetsCycle
[rulesetsCycle.name]: rulesetsCycle,
[securityFeaturesCycle.name]: securityFeaturesCycle
};

// node_modules/@intentius/chant/src/audit/fetch.ts
Expand Down
2 changes: 2 additions & 0 deletions src/cli/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { repoSettingsCycle } from "../cycles/repo-settings.js";
import { membershipCycle } from "../cycles/membership.js";
import { teamsCycle } from "../cycles/teams.js";
import { rulesetsCycle } from "../cycles/rulesets.js";
import { securityFeaturesCycle } from "../cycles/security-features.js";

/**
* Registry of all available governance cycles, keyed by the name accepted by
Expand All @@ -30,4 +31,5 @@ export const CYCLE_REGISTRY: Record<string, Cycle> = {
[membershipCycle.name]: membershipCycle,
[teamsCycle.name]: teamsCycle,
[rulesetsCycle.name]: rulesetsCycle,
[securityFeaturesCycle.name]: securityFeaturesCycle,
};
34 changes: 34 additions & 0 deletions src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,35 @@ export interface MemberConfig {
role?: OrgMemberRole;
}

// ---------------------------------------------------------------------------
// Repository security features
// ---------------------------------------------------------------------------

/**
* Repository security-feature toggles. Absent fields are not managed.
*
* The first three map to the repo `security_and_analysis` object (set via
* `PATCH /repos/{o}/{r}`); the last two use dedicated endpoints
* (`vulnerability-alerts`, `automated-security-fixes`).
*
* License-gated note: GitHub Advanced Security features (`advancedSecurity`,
* and secret scanning on private repos) require a GHAS license. Where a feature
* is unavailable, GitHub rejects the enabling write; the cycle surfaces that as
* a reported failed entry rather than crashing the run (see cycle header).
*/
export interface RepoSecurityConfig {
/** GitHub Advanced Security (`security_and_analysis.advanced_security`). */
advancedSecurity?: boolean;
/** Secret scanning (`security_and_analysis.secret_scanning`). */
secretScanning?: boolean;
/** Secret scanning push protection (`security_and_analysis.secret_scanning_push_protection`). */
secretScanningPushProtection?: boolean;
/** Dependabot vulnerability alerts (`vulnerability-alerts` endpoint). */
vulnerabilityAlerts?: boolean;
/** Dependabot automated security fixes (`automated-security-fixes` endpoint). */
dependabotSecurityUpdates?: boolean;
}

// ---------------------------------------------------------------------------
// Rulesets (repo + org)
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -207,6 +236,11 @@ export interface RepoConfig {
* Absent means repo rulesets are not managed by chant.
*/
rulesets?: RulesetConfig[];
/**
* Repository security features (GHAS, secret scanning, Dependabot).
* Absent means security features are not managed by chant.
*/
security?: RepoSecurityConfig;
}

// ---------------------------------------------------------------------------
Expand Down
Loading
Loading