@@ -233710,8 +233710,49 @@ function shouldFail(report, failOn) {
233710233710 }
233711233711}
233712233712
233713+ // src/report/identity.ts
233714+ function buildIdentityReport(installations, memberLogins, machineUserLogins) {
233715+ const apps = installations.map((i) => ({
233716+ slug: i.app_slug ?? "unknown",
233717+ appId: i.app_id,
233718+ permissionCount: i.permissions ? Object.keys(i.permissions).length : 0
233719+ }));
233720+ const memberSet = new Set(memberLogins);
233721+ const declared = [...new Set(machineUserLogins)];
233722+ const flagged = declared.filter((l) => memberSet.has(l));
233723+ const notMembers = declared.filter((l) => !memberSet.has(l));
233724+ const recommendations = [];
233725+ for (const login of flagged) {
233726+ recommendations.push(
233727+ `Machine user "${login}" consumes an org seat \u2014 replace it with a GitHub App (Apps consume no seat).`
233728+ );
233729+ }
233730+ return {
233731+ installations: { count: apps.length, apps },
233732+ machineUsers: { flagged, notMembers },
233733+ summary: { installationCount: apps.length, flaggedMachineUsers: flagged.length },
233734+ recommendations
233735+ };
233736+ }
233737+ function renderIdentityReport(report) {
233738+ const lines = [];
233739+ lines.push("--- identity & service-account hygiene ---");
233740+ lines.push(` installed apps: ${report.installations.count} (seat-free)`);
233741+ for (const a of report.installations.apps) {
233742+ lines.push(` ${a.slug} permissions=${a.permissionCount}`);
233743+ }
233744+ lines.push(
233745+ ` machine users: ${report.machineUsers.flagged.length} flagged` + (report.machineUsers.notMembers.length ? `, ${report.machineUsers.notMembers.length} declared-not-member` : "")
233746+ );
233747+ for (const r of report.recommendations) {
233748+ lines.push(` \u26A0 ${r}`);
233749+ }
233750+ lines.push("");
233751+ return lines.join("\n");
233752+ }
233753+
233713233754// src/report/compliance.ts
233714- function buildComplianceReport(results, audit) {
233755+ function buildComplianceReport(results, audit, identity ) {
233715233756 const cycles = [];
233716233757 const errored = [];
233717233758 const deferred = [];
@@ -233757,11 +233798,12 @@ function buildComplianceReport(results, audit) {
233757233798 mergeWorthy: auditMergeWorthy
233758233799 };
233759233800 }
233760- const clean = drift === 0 && guardrailTrips === 0 && failed === 0 && errored.length === 0 && deferred.length === 0 && auditMergeWorthy === 0;
233801+ const clean = drift === 0 && guardrailTrips === 0 && failed === 0 && errored.length === 0 && deferred.length === 0 && auditMergeWorthy === 0 && (identity?.summary.flaggedMachineUsers ?? 0) === 0 ;
233761233802 return {
233762233803 modes: [...modeSet],
233763233804 cycles,
233764233805 audit: auditCompliance,
233806+ identity,
233765233807 totals: {
233766233808 drift,
233767233809 guardrailTrips,
@@ -233812,6 +233854,10 @@ function renderComplianceReport(report) {
233812233854 ` total=${report.audit.total} merge-worthy=${report.audit.mergeWorthy} (quick-win=${report.audit.quickWin}, needs-review=${report.audit.needsReview}, report-only=${report.audit.reportOnly})`
233813233855 );
233814233856 }
233857+ if (report.identity) {
233858+ lines.push("");
233859+ lines.push(renderIdentityReport(report.identity).trimEnd());
233860+ }
233815233861 lines.push("");
233816233862 lines.push("--- totals ---");
233817233863 const t = report.totals;
@@ -234020,6 +234066,7 @@ function parseReportArgs(argv) {
234020234066 cycles: [],
234021234067 out: void 0,
234022234068 audit: false,
234069+ identity: false,
234023234070 failOn: "none"
234024234071 };
234025234072 const knownFlags = /* @__PURE__ */ new Set([
@@ -234030,6 +234077,7 @@ function parseReportArgs(argv) {
234030234077 "--cycles",
234031234078 "--out",
234032234079 "--audit",
234080+ "--identity",
234033234081 "--fail-on"
234034234082 ]);
234035234083 let i = 0;
@@ -234079,6 +234127,10 @@ function parseReportArgs(argv) {
234079234127 args.audit = true;
234080234128 break;
234081234129 }
234130+ case "--identity": {
234131+ args.identity = true;
234132+ break;
234133+ }
234082234134 case "--fail-on": {
234083234135 const val = argv[++i];
234084234136 if (val !== "none" && val !== "attention") {
@@ -234544,7 +234596,39 @@ async function runReport(argv) {
234544234596 }
234545234597 }
234546234598 }
234547- const report = buildComplianceReport([result], auditReport);
234599+ let identityReport;
234600+ if (reportArgs.identity) {
234601+ try {
234602+ const installations = [];
234603+ const memberLogins = [];
234604+ const machineUsers = [];
234605+ for (const [orgName, orgCfg] of Object.entries(config2.orgs)) {
234606+ try {
234607+ const data = await client.request(
234608+ "GET",
234609+ `/orgs/${orgName}/installations?per_page=100`
234610+ );
234611+ installations.push(...data.installations ?? []);
234612+ } catch (err) {
234613+ if (!(err instanceof Error && (err.message.includes("404") || err.message.includes("403")))) throw err;
234614+ }
234615+ try {
234616+ const members = await client.request(
234617+ "GET",
234618+ `/orgs/${orgName}/members?per_page=100`
234619+ );
234620+ for (const m of members ?? []) if (typeof m.login === "string") memberLogins.push(m.login);
234621+ } catch (err) {
234622+ if (!(err instanceof Error && (err.message.includes("404") || err.message.includes("403")))) throw err;
234623+ }
234624+ machineUsers.push(...orgCfg.machineUsers ?? []);
234625+ }
234626+ identityReport = buildIdentityReport(installations, memberLogins, machineUsers);
234627+ } catch (err) {
234628+ die(3, `identity pass failed: ${errMsg(err)}`);
234629+ }
234630+ }
234631+ const report = buildComplianceReport([result], auditReport, identityReport);
234548234632 report.generatedAt = (/* @__PURE__ */ new Date()).toISOString();
234549234633 process.stdout.write(renderComplianceReport(report));
234550234634 if (reportArgs.out) {
@@ -234595,6 +234679,7 @@ function printUsage() {
234595234679 " --cycles <name[,name...]> Cycles to include (default: all).",
234596234680 " --out <path> Write the JSON compliance artifact to this path.",
234597234681 " --audit Include an audit pass in the report.",
234682+ " --identity Include an identity & service-account hygiene pass.",
234598234683 " --fail-on none|attention Exit 4 when the report needs attention (default: none).",
234599234684 "",
234600234685 "Exit codes:",
0 commit comments