130 行
6.1 KiB
JavaScript
130 行
6.1 KiB
JavaScript
"use strict";
|
|
|
|
const path = require("node:path");
|
|
|
|
const OBSERVATION_WINDOW_DAYS = 7;
|
|
|
|
const SAFE_RESULT_KEYS = Object.freeze([
|
|
"abnormalActiveSessions",
|
|
"activeEmployeesWithSession",
|
|
"activeSessionsTotal",
|
|
"activeTotal",
|
|
"bindingConflictGroups",
|
|
"bindingCovered",
|
|
"bindingMissing",
|
|
"bindingMutationAudits7d",
|
|
"directoryMutationAudits7d",
|
|
"emailLoginLocked",
|
|
"inactiveBindingRows",
|
|
"mustChangePassword",
|
|
"passwordLoginCoveredAllTime",
|
|
"passwordLoginCovered7d",
|
|
"passwordLocked",
|
|
"passwordResetLocked",
|
|
"roleMutationAudits7d",
|
|
"wecomLoginCoveredAllTime",
|
|
"wecomLoginCovered7d",
|
|
"wecomLoginEvents7d",
|
|
]);
|
|
|
|
const READINESS_SQL = String.raw`
|
|
WITH active AS (
|
|
SELECT id
|
|
FROM employees
|
|
WHERE status = 'active' AND archived_at IS NULL
|
|
), binding_conflicts AS (
|
|
SELECT count(*)::int AS groups
|
|
FROM (
|
|
SELECT employee_id::text
|
|
FROM wecom_identity_bindings
|
|
GROUP BY employee_id
|
|
HAVING count(*) > 1
|
|
UNION ALL
|
|
SELECT external_user_id
|
|
FROM wecom_identity_bindings
|
|
GROUP BY external_user_id
|
|
HAVING count(*) > 1
|
|
) conflicts
|
|
), metrics AS (
|
|
SELECT
|
|
(SELECT count(*) FROM active)::int AS "activeTotal",
|
|
(SELECT count(*) FROM employees e JOIN active a ON a.id = e.id WHERE e.must_change_password)::int AS "mustChangePassword",
|
|
(SELECT count(*) FROM employees e JOIN active a ON a.id = e.id WHERE e.login_locked_until > now())::int AS "passwordLocked",
|
|
(SELECT count(*) FROM employees e JOIN active a ON a.id = e.id WHERE e.email_login_code_locked_until > now())::int AS "emailLoginLocked",
|
|
(SELECT count(*) FROM employees e JOIN active a ON a.id = e.id WHERE e.password_reset_locked_until > now())::int AS "passwordResetLocked",
|
|
(SELECT count(DISTINCT b.employee_id) FROM wecom_identity_bindings b JOIN active a ON a.id = b.employee_id)::int AS "bindingCovered",
|
|
(SELECT count(*) FROM active a LEFT JOIN wecom_identity_bindings b ON b.employee_id = a.id WHERE b.id IS NULL)::int AS "bindingMissing",
|
|
(SELECT groups FROM binding_conflicts)::int AS "bindingConflictGroups",
|
|
(SELECT count(*) FROM wecom_identity_bindings b JOIN employees e ON e.id = b.employee_id WHERE e.status <> 'active' OR e.archived_at IS NOT NULL)::int AS "inactiveBindingRows",
|
|
(SELECT count(DISTINCT l.actor_id) FROM audit_logs l JOIN active a ON a.id = l.actor_id WHERE l.action = 'AUTH_WECOM_LOGIN_SUCCESS')::int AS "wecomLoginCoveredAllTime",
|
|
(SELECT count(DISTINCT l.actor_id) FROM audit_logs l JOIN active a ON a.id = l.actor_id WHERE l.action = 'AUTH_WECOM_LOGIN_SUCCESS' AND l.created_at >= now() - interval '7 days')::int AS "wecomLoginCovered7d",
|
|
(SELECT count(*) FROM audit_logs l JOIN active a ON a.id = l.actor_id WHERE l.action = 'AUTH_WECOM_LOGIN_SUCCESS' AND l.created_at >= now() - interval '7 days')::int AS "wecomLoginEvents7d",
|
|
(SELECT count(DISTINCT l.actor_id) FROM audit_logs l JOIN active a ON a.id = l.actor_id WHERE l.action = 'AUTH_LOGIN_SUCCESS')::int AS "passwordLoginCoveredAllTime",
|
|
(SELECT count(DISTINCT l.actor_id) FROM audit_logs l JOIN active a ON a.id = l.actor_id WHERE l.action = 'AUTH_LOGIN_SUCCESS' AND l.created_at >= now() - interval '7 days')::int AS "passwordLoginCovered7d",
|
|
(SELECT count(*) FROM audit_logs WHERE action = 'SYSTEM_USER_ROLES_SET' AND created_at >= now() - interval '7 days')::int AS "roleMutationAudits7d",
|
|
(SELECT count(*) FROM audit_logs WHERE action LIKE 'WECOM_DIRECTORY_%' AND created_at >= now() - interval '7 days')::int AS "directoryMutationAudits7d",
|
|
(SELECT count(*) FROM audit_logs WHERE action = 'AUTH_WECOM_BIND' AND created_at >= now() - interval '7 days')::int AS "bindingMutationAudits7d",
|
|
(SELECT count(*) FROM auth_sessions s WHERE s.revoked_at IS NULL AND s.expires_at > now())::int AS "activeSessionsTotal",
|
|
(SELECT count(DISTINCT s.employee_id) FROM auth_sessions s JOIN active a ON a.id = s.employee_id WHERE s.revoked_at IS NULL AND s.expires_at > now())::int AS "activeEmployeesWithSession",
|
|
(SELECT count(*) FROM auth_sessions s JOIN employees e ON e.id = s.employee_id WHERE s.revoked_at IS NULL AND s.expires_at > now() AND (e.status <> 'active' OR e.archived_at IS NOT NULL))::int AS "abnormalActiveSessions"
|
|
)
|
|
SELECT * FROM metrics
|
|
`;
|
|
|
|
function assertSafeMetrics(metrics) {
|
|
const keys = Object.keys(metrics).sort();
|
|
if (JSON.stringify(keys) !== JSON.stringify([...SAFE_RESULT_KEYS].sort())) {
|
|
throw new Error("result_field_contract_invalid");
|
|
}
|
|
if (Object.values(metrics).some((value) => !Number.isInteger(value) || value < 0)) {
|
|
throw new Error("result_value_contract_invalid");
|
|
}
|
|
}
|
|
|
|
async function run() {
|
|
const appRoot = path.resolve(process.env.AUTH_CENSUS_APP_ROOT || process.cwd());
|
|
const { Client } = require(path.join(appRoot, "node_modules/pg"));
|
|
const { getConfig } = require(path.join(appRoot, "src/config.js"));
|
|
const config = getConfig();
|
|
const client = new Client({ connectionString: process.env.DATABASE_URL });
|
|
let inTransaction = false;
|
|
|
|
try {
|
|
await client.connect();
|
|
await client.query("BEGIN TRANSACTION READ ONLY");
|
|
inTransaction = true;
|
|
await client.query("SET LOCAL statement_timeout = '30s'");
|
|
await client.query("SET LOCAL lock_timeout = '2s'");
|
|
const readOnly = await client.query("SELECT current_setting('transaction_read_only') AS value");
|
|
if (readOnly.rows[0]?.value !== "on") throw new Error("transaction_not_read_only");
|
|
|
|
const result = await client.query(READINESS_SQL);
|
|
const metrics = result.rows[0];
|
|
assertSafeMetrics(metrics);
|
|
await client.query("COMMIT");
|
|
inTransaction = false;
|
|
|
|
process.stdout.write(`${JSON.stringify({
|
|
status: "PASS",
|
|
transactionReadOnly: true,
|
|
sourceBuildSha: config.buildSha,
|
|
observationWindowDays: OBSERVATION_WINDOW_DAYS,
|
|
metrics,
|
|
}, null, 2)}\n`);
|
|
} catch {
|
|
if (inTransaction) {
|
|
try { await client.query("ROLLBACK"); } catch { /* fail closed */ }
|
|
}
|
|
process.stderr.write("[auth-readiness-census][FAILED] code=QUERY_FAILED\n");
|
|
process.exitCode = 1;
|
|
} finally {
|
|
await client.end().catch(() => {});
|
|
}
|
|
}
|
|
|
|
module.exports = { OBSERVATION_WINDOW_DAYS, READINESS_SQL, SAFE_RESULT_KEYS, assertSafeMetrics };
|
|
|
|
if (require.main === module || process.argv[1] === "-") {
|
|
run();
|
|
}
|