520 行
18 KiB
Bash
可执行文件
520 行
18 KiB
Bash
可执行文件
#!/usr/bin/env bash
|
|
set -u -o pipefail
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
CLIENT_DIR="$ROOT_DIR/client"
|
|
PUBLIC_BASE_URL="${LIVE_SMOKE_BASE_URL:-https://oa.hyinda.com}"
|
|
REPORT_DIR="${OAOPS1_REPORT_DIR:-$ROOT_DIR/artifacts/qa-flow/oa-real-use-daily-$(date +%Y%m%d-%H%M%S)}"
|
|
SUMMARY_TXT="$REPORT_DIR/summary.txt"
|
|
STATUS=0
|
|
FINDING_COUNT=0
|
|
|
|
mkdir -p "$REPORT_DIR"
|
|
: >"$SUMMARY_TXT"
|
|
|
|
log() {
|
|
printf '%s\n' "$*" | tee -a "$SUMMARY_TXT"
|
|
}
|
|
|
|
write_step_json() {
|
|
local name="$1"
|
|
local status="$2"
|
|
local kind="$3"
|
|
local code="${4:-0}"
|
|
local detail="${5:-}"
|
|
node - "$REPORT_DIR" "$name" "$status" "$kind" "$code" "$detail" <<'NODE' || true
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const [reportDir, name, status, kind, code, detail] = process.argv.slice(2);
|
|
fs.writeFileSync(path.join(reportDir, `${name}.step.json`), `${JSON.stringify({
|
|
name,
|
|
status,
|
|
kind,
|
|
code: Number(code || 0),
|
|
detail: String(detail || "").slice(0, 600),
|
|
logPath: path.resolve(path.join(reportDir, `${name}.log`)),
|
|
checkedAt: new Date().toISOString(),
|
|
}, null, 2)}\n`, "utf8");
|
|
NODE
|
|
}
|
|
|
|
run_step() {
|
|
local name="$1"
|
|
shift
|
|
local log_file="$REPORT_DIR/${name}.log"
|
|
|
|
log ">>> [oaops1] $name"
|
|
if "$@" > >(tee "$log_file") 2>&1; then
|
|
log "[oaops1][PASS] $name"
|
|
write_step_json "$name" "PASS" "required" 0 "ok"
|
|
else
|
|
local code="$?"
|
|
log "[oaops1][FAIL] $name exit=$code"
|
|
STATUS=1
|
|
write_step_json "$name" "FAIL" "required" "$code" "required step failed"
|
|
fi
|
|
}
|
|
|
|
run_finding_step() {
|
|
local name="$1"
|
|
shift
|
|
local log_file="$REPORT_DIR/${name}.log"
|
|
|
|
log ">>> [oaops1] $name"
|
|
if "$@" > >(tee "$log_file") 2>&1; then
|
|
log "[oaops1][PASS] $name"
|
|
write_step_json "$name" "PASS" "finding_allowed" 0 "ok"
|
|
else
|
|
local code="$?"
|
|
log "[oaops1][FINDING] $name exit=$code"
|
|
FINDING_COUNT=$((FINDING_COUNT + 1))
|
|
write_step_json "$name" "FINDING" "finding_allowed" "$code" "finding or missing runtime credential"
|
|
fi
|
|
}
|
|
|
|
run_client_finding_step() {
|
|
local name="$1"
|
|
shift
|
|
local log_file="$REPORT_DIR/${name}.log"
|
|
|
|
log ">>> [oaops1] $name"
|
|
if (cd "$CLIENT_DIR" && env "$@") > >(tee "$log_file") 2>&1; then
|
|
log "[oaops1][PASS] $name"
|
|
write_step_json "$name" "PASS" "finding_allowed" 0 "ok"
|
|
else
|
|
local code="$?"
|
|
log "[oaops1][FINDING] $name exit=$code"
|
|
FINDING_COUNT=$((FINDING_COUNT + 1))
|
|
write_step_json "$name" "FINDING" "finding_allowed" "$code" "finding or missing runtime credential"
|
|
fi
|
|
}
|
|
|
|
run_client_spec_if_present() {
|
|
local name="$1"
|
|
local spec="$2"
|
|
shift 2
|
|
|
|
if [[ ! -f "$CLIENT_DIR/$spec" ]]; then
|
|
log "[oaops1][FINDING] $name missing client spec: $spec"
|
|
FINDING_COUNT=$((FINDING_COUNT + 1))
|
|
write_step_json "$name" "FINDING" "finding_allowed" 0 "client spec missing: $spec"
|
|
return 0
|
|
fi
|
|
|
|
local cooldown="${OAOPS1_E2E_COOLDOWN_SECONDS:-2}"
|
|
if [[ "$cooldown" =~ ^[0-9]+$ && "$cooldown" -gt 0 ]]; then
|
|
sleep "$cooldown"
|
|
fi
|
|
run_client_finding_step "$name" "$@" npx playwright test "$spec" --workers=1 --reporter=list
|
|
}
|
|
|
|
cooldown_login_step() {
|
|
local reason="$1"
|
|
local cooldown="${OAOPS1_LOGIN_STEP_COOLDOWN_SECONDS:-65}"
|
|
if [[ "$cooldown" =~ ^[0-9]+$ && "$cooldown" -gt 0 ]]; then
|
|
log "[oaops1] login cooldown ${cooldown}s before ${reason}"
|
|
sleep "$cooldown"
|
|
fi
|
|
}
|
|
|
|
load_default_env() {
|
|
local env_file="${OAOPS1_ENV_FILE:-$HOME/.hyb-oa/live-smoke.env}"
|
|
if [[ "${OAOPS1_SKIP_ENV_FILE:-0}" == "1" || ! -f "$env_file" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
set -a
|
|
# shellcheck disable=SC1090
|
|
source "$env_file"
|
|
set +a
|
|
PUBLIC_BASE_URL="${LIVE_SMOKE_BASE_URL:-$PUBLIC_BASE_URL}"
|
|
log "[oaops1] loaded masked live smoke env file"
|
|
}
|
|
|
|
run_ready_and_runtime() {
|
|
run_step "public-ready" curl -fsS "$PUBLIC_BASE_URL/ready"
|
|
run_finding_step "live-smoke-env" env LIVE_SMOKE_BASE_URL="$PUBLIC_BASE_URL" node "$ROOT_DIR/scripts/qa/check-live-smoke-env.mjs" --json
|
|
run_finding_step "live-readiness" env QA_ISSUE_ID=OAOPS1 LIVE_SMOKE_BASE_URL="$PUBLIC_BASE_URL" LIVE_SMOKE_READINESS_DIR="$REPORT_DIR/live-smoke-readiness" bash "$ROOT_DIR/scripts/qa/run-live-smoke-readiness.sh"
|
|
run_finding_step "oa-only-smoke-readiness" env LIVE_SMOKE_BASE_URL="$PUBLIC_BASE_URL" OA_ONLY_SMOKE_READINESS_REPORT_DIR="$REPORT_DIR/oa-only-smoke-readiness" node "$ROOT_DIR/scripts/qa/check-oa-only-smoke-readiness.mjs"
|
|
cooldown_login_step "live-feature-flags"
|
|
run_finding_step "live-feature-flags" env LIVE_SMOKE_BASE_URL="$PUBLIC_BASE_URL" node "$ROOT_DIR/scripts/qa/check-live-feature-flags.mjs" \
|
|
--expect payment_batch_v1=force_on \
|
|
--expect erp_sync=force_off \
|
|
--expect attendance_v1=disabled \
|
|
--expect attendance_unified_v1=enabled \
|
|
--json
|
|
}
|
|
|
|
run_authenticated_readonly_probe() {
|
|
node - "$PUBLIC_BASE_URL" "$REPORT_DIR" <<'NODE'
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
const [baseUrlRaw, reportDir] = process.argv.slice(2);
|
|
const baseUrl = String(baseUrlRaw || "https://oa.hyinda.com").replace(/\/$/, "");
|
|
|
|
const credentials = {
|
|
admin: {
|
|
identifier: process.env.LIVE_SMOKE_ADMIN_IDENTIFIER?.trim(),
|
|
password: process.env.LIVE_SMOKE_ADMIN_PASSWORD?.trim(),
|
|
},
|
|
};
|
|
|
|
const probes = [
|
|
{ id: "portal", role: "admin", path: "/api/portal/home", domain: "portal" },
|
|
{ id: "workflow-tasks", role: "admin", path: "/api/workflow/tasks?page=1&pageSize=5", domain: "approval" },
|
|
{ id: "reimbursements", role: "admin", path: "/api/reimbursements?page=1&pageSize=5", domain: "reimbursement" },
|
|
{ id: "reimbursement-summary", role: "admin", path: "/api/reimbursements/stats/summary", domain: "reimbursement" },
|
|
{ id: "invoice-pool", role: "admin", path: "/api/invoice-pool?page=1&pageSize=5", domain: "invoice_ocr" },
|
|
{ id: "purchase-requests", role: "admin", path: "/api/purchase-requests?page=1&pageSize=5", domain: "procurement" },
|
|
{ id: "contracts-registry", role: "admin", path: "/api/contracts-registry", domain: "contracts" },
|
|
{ id: "projects", role: "admin", path: "/api/projects?page=1&pageSize=5", domain: "projects" },
|
|
{ id: "audit-logs", role: "admin", path: "/api/audit-logs?page=1&pageSize=5", domain: "audit" },
|
|
];
|
|
|
|
function maskIdentifier(value) {
|
|
if (!value) return null;
|
|
const text = String(value);
|
|
if (text.length <= 4) return "***";
|
|
return `${text.slice(0, 2)}***${text.slice(-2)}`;
|
|
}
|
|
|
|
async function fetchJson(url, init = {}) {
|
|
const response = await fetch(url, {
|
|
...init,
|
|
signal: AbortSignal.timeout(20_000),
|
|
});
|
|
const text = await response.text();
|
|
let json = null;
|
|
try {
|
|
json = text ? JSON.parse(text) : null;
|
|
} catch {
|
|
json = null;
|
|
}
|
|
return { ok: response.ok, status: response.status, json, text: text.slice(0, 200) };
|
|
}
|
|
|
|
function summarizePayload(json) {
|
|
const data = json?.data;
|
|
if (Array.isArray(data)) return { itemCount: data.length };
|
|
if (Array.isArray(data?.items)) return { itemCount: data.items.length, total: data.total ?? null };
|
|
if (typeof data === "object" && data) {
|
|
const summary = {};
|
|
for (const [key, value] of Object.entries(data)) {
|
|
if (/token|secret|password|phone|email|payload|attachment|comment|form/i.test(key)) continue;
|
|
if (typeof value === "number" || typeof value === "boolean") summary[key] = value;
|
|
if (typeof value === "string" && value.length <= 60) summary[key] = value;
|
|
}
|
|
return summary;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
async function login(role) {
|
|
const credential = credentials[role];
|
|
if (!credential?.identifier || !credential?.password) {
|
|
return { status: "SKIP", reason: `missing_${role}_credentials` };
|
|
}
|
|
const result = await fetchJson(`${baseUrl}/api/auth/login`, {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
"x-device-fingerprint": `oaops1-${role}-readonly`,
|
|
},
|
|
body: JSON.stringify({
|
|
identifier: credential.identifier,
|
|
password: credential.password,
|
|
}),
|
|
});
|
|
const accessToken = result.json?.data?.accessToken;
|
|
if (!result.ok || typeof accessToken !== "string" || !accessToken) {
|
|
return {
|
|
status: "FAIL",
|
|
reason: `login_http_${result.status}`,
|
|
code: result.json?.error?.code ?? null,
|
|
};
|
|
}
|
|
return {
|
|
status: "PASS",
|
|
accessToken,
|
|
maskedIdentifier: maskIdentifier(credential.identifier),
|
|
};
|
|
}
|
|
|
|
async function main() {
|
|
const roles = {};
|
|
for (const role of Object.keys(credentials)) {
|
|
roles[role] = await login(role);
|
|
}
|
|
|
|
const checks = [];
|
|
for (const probe of probes) {
|
|
const roleState = roles[probe.role];
|
|
if (roleState.status !== "PASS") {
|
|
checks.push({
|
|
...probe,
|
|
status: "SKIP",
|
|
reason: roleState.reason || `${probe.role}_login_not_available`,
|
|
});
|
|
continue;
|
|
}
|
|
const result = await fetchJson(`${baseUrl}${probe.path}`, {
|
|
headers: {
|
|
accept: "application/json",
|
|
authorization: `Bearer ${roleState.accessToken}`,
|
|
},
|
|
});
|
|
checks.push({
|
|
...probe,
|
|
status: result.ok ? "PASS" : "FAIL",
|
|
httpStatus: result.status,
|
|
summary: result.ok ? summarizePayload(result.json) : {
|
|
code: result.json?.error?.code ?? null,
|
|
message: typeof result.json?.error?.message === "string" ? result.json.error.message.slice(0, 100) : null,
|
|
},
|
|
});
|
|
}
|
|
|
|
const blocking = checks.filter((item) => item.status === "FAIL");
|
|
const skipped = checks.filter((item) => item.status === "SKIP");
|
|
const status = blocking.length ? "FAIL" : skipped.length ? "PASS_WITH_FINDINGS" : "PASS";
|
|
const summary = {
|
|
mode: "oaops1-authenticated-readonly-probe",
|
|
status,
|
|
baseUrl,
|
|
checkedAt: new Date().toISOString(),
|
|
readOnly: true,
|
|
duplicateRoleChecks: "employee/finance/payment/AP are covered by oa-only-smoke-readiness to avoid repeated login throttling",
|
|
roles: Object.fromEntries(Object.entries(roles).map(([role, state]) => [role, {
|
|
status: state.status,
|
|
reason: state.reason || null,
|
|
maskedIdentifier: state.maskedIdentifier || null,
|
|
}])),
|
|
checks,
|
|
boundaries: {
|
|
noBusinessWrites: true,
|
|
noExportsByDefault: true,
|
|
noFileUploads: true,
|
|
noWeComWrites: true,
|
|
noRealPaymentExecution: true,
|
|
},
|
|
};
|
|
fs.writeFileSync(path.join(reportDir, "authenticated-readonly-probe.json"), `${JSON.stringify(summary, null, 2)}\n`, "utf8");
|
|
console.log(JSON.stringify(summary, null, 2));
|
|
if (blocking.length) process.exit(1);
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(`[oaops1-probe][ERROR] ${error instanceof Error ? error.message : String(error)}`);
|
|
process.exit(1);
|
|
});
|
|
NODE
|
|
}
|
|
|
|
run_mobile_and_entry_matrix() {
|
|
run_client_spec_if_present "mobile-entry-matrix" "tests/e2e/r124-mobile-entry-matrix.spec.ts" E2E_BASE_URL="$PUBLIC_BASE_URL"
|
|
run_client_spec_if_present "wecom-rendering-matrix" "tests/e2e/r6-wecom-mobile-rendering-hardening.spec.ts" E2E_BASE_URL="$PUBLIC_BASE_URL"
|
|
run_client_spec_if_present "wecom-entry" "tests/e2e/wecom-entry.spec.ts" E2E_BASE_URL="$PUBLIC_BASE_URL"
|
|
}
|
|
|
|
write_manual_checklist() {
|
|
cat >"$REPORT_DIR/manual-gates.md" <<'EOF'
|
|
# OAOPS1 每日人工收口清单
|
|
|
|
## 员工
|
|
- 真实报销:上传 PDF 发票,补充必要附件,提交 OA 审批。
|
|
- 采购/费用:在 OA 创建申请,关联项目;如涉及合同,关联或补录合同台账。
|
|
- 合同:只录入合同摘要、编号、负责人、项目、金额、线下归档位置;涉密合同正文、敏感报价和成本细节不上传公网 OA。
|
|
|
|
## 审批人
|
|
- 从 OA 待办进入详情,确认报销、采购、合同或项目上下文后再审批。
|
|
- 退回时写清需要补充的材料,避免线下口头说明替代系统记录。
|
|
|
|
## 出纳/财务
|
|
- 审批通过后创建付款批次,线下银行渠道付款。
|
|
- 上传银行回单附件后再在 OA 标记已付款。
|
|
- 每日抽查 AP 台账、报销导出、票据状态和付款批次状态。
|
|
|
|
## 管理员
|
|
- 查看本报告 summary.json 和人工问题登记。
|
|
- 阻断项当天建修复 Issue CSV 或暂停对应流程;非阻断体验问题进入反馈池。
|
|
- 确认 payment_batch_v1=force_on,erp_sync=force_off;图片 OCR、预算、ERP、银行流水导入、真实支付直连继续冻结。
|
|
EOF
|
|
}
|
|
|
|
run_privacy_smoke() {
|
|
local sensitive_pattern='(["'\''"]?(WECOM_[A-Z0-9_]*(SECRET|TOKEN)|[A-Z0-9_]*(TOKEN|SECRET|PASSWORD|ENCODING_AES_KEY|ENCODINGAESKEY|CORP_SECRET|ACCESS_TOKEN)[A-Z0-9_]*)["'\''"]?[[:space:]]*[:=][[:space:]]*["'\''"]?[A-Za-z0-9._:/+=-]{8,}["'\''"]?)|Authorization:[[:space:]]*Bearer[[:space:]]+[A-Za-z0-9._-]{8,}|access_token=[A-Za-z0-9._-]{8,}|1[3-9][0-9]{9}|[[:alnum:]._%+-]+@[[:alnum:].-]+\.[[:alpha:]]{2,}|-----BEGIN|BEGIN [A-Z ]*KEY|"((token|secret|password|EncodingAESKey|encodingAESKey|sp_no|spNo|template_id|templateId|userid|userId|external_userid|externalUserId))"[[:space:]]*:[[:space:]]*"[^"*]{8,}"|\b20[0-9]{12,}\b|"(raw|rawPayload|raw_payload|payload|xml|ciphertext|Encrypt|formValue|formValues|attachments|comments)"[[:space:]]*:'
|
|
local matches
|
|
matches="$(grep -RInE "$sensitive_pattern" "$REPORT_DIR" || true)"
|
|
if [[ -n "$matches" ]]; then
|
|
printf '%s\n' "$matches"
|
|
log "[oaops1][FAIL] report-privacy-smoke matched sensitive pattern"
|
|
STATUS=1
|
|
else
|
|
log "[oaops1][PASS] report-privacy-smoke"
|
|
fi
|
|
}
|
|
|
|
write_summary_json() {
|
|
node - "$ROOT_DIR" "$REPORT_DIR" "$PUBLIC_BASE_URL" "$STATUS" "$FINDING_COUNT" <<'NODE' || return 1
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const { execSync } = require("node:child_process");
|
|
|
|
const [rootDir, reportDir, baseURL, statusInput, findingCountInput] = process.argv.slice(2);
|
|
|
|
function readJson(file) {
|
|
if (!file || !fs.existsSync(file)) return null;
|
|
try {
|
|
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function step(name) {
|
|
return readJson(path.join(reportDir, `${name}.step.json`)) || {
|
|
name,
|
|
status: "MISSING",
|
|
kind: "unknown",
|
|
logPath: path.resolve(path.join(reportDir, `${name}.log`)),
|
|
};
|
|
}
|
|
|
|
function child(relativePath) {
|
|
const file = path.join(reportDir, relativePath, "summary.json");
|
|
const json = readJson(file);
|
|
return {
|
|
status: json?.status || json?.conclusion || "MISSING",
|
|
reportPath: fs.existsSync(file) ? path.resolve(file) : null,
|
|
};
|
|
}
|
|
|
|
let commit = "unknown";
|
|
try {
|
|
commit = execSync("git rev-parse --short HEAD", { cwd: rootDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
} catch {}
|
|
|
|
const stepNames = [
|
|
"public-ready",
|
|
"live-smoke-env",
|
|
"live-readiness",
|
|
"oa-only-smoke-readiness",
|
|
"live-feature-flags",
|
|
"authenticated-readonly-probe",
|
|
"mobile-entry-matrix",
|
|
"wecom-rendering-matrix",
|
|
"wecom-entry",
|
|
];
|
|
const steps = Object.fromEntries(stepNames.map((name) => [name, step(name)]));
|
|
const readonlyProbe = readJson(path.join(reportDir, "authenticated-readonly-probe.json"));
|
|
|
|
const findings = [];
|
|
for (const [name, item] of Object.entries(steps)) {
|
|
const status = String(item.status || "UNKNOWN");
|
|
if (status === "PASS") continue;
|
|
if (status === "FINDING") {
|
|
findings.push({ source: name, status, severity: "finding" });
|
|
} else if (/FAIL|ERROR|BLOCKED/i.test(status)) {
|
|
findings.push({ source: name, status, severity: item.kind === "required" ? "blocker" : "finding" });
|
|
} else if (/SKIP|MISSING|PAUSED|PARTIAL/i.test(status)) {
|
|
findings.push({ source: name, status, severity: "manual_or_config" });
|
|
}
|
|
}
|
|
if (readonlyProbe?.status && readonlyProbe.status !== "PASS") {
|
|
findings.push({ source: "authenticated-readonly-probe", status: readonlyProbe.status, severity: readonlyProbe.status === "FAIL" ? "finding" : "manual_or_config" });
|
|
}
|
|
|
|
let status = "PASS_OA_REAL_USE_DAILY_READY";
|
|
if (Number(statusInput || 0) !== 0 || findings.some((item) => item.severity === "blocker")) {
|
|
status = "SKIP/BLOCKED_BY_EXTERNAL_CONFIG";
|
|
} else if (findings.length || Number(findingCountInput || 0) > 0) {
|
|
status = "PASS_WITH_FINDINGS";
|
|
}
|
|
|
|
const summary = {
|
|
issueId: "OAOPS1",
|
|
mode: "oa-real-use-daily",
|
|
status,
|
|
conclusion: status,
|
|
baseURL,
|
|
commit,
|
|
checkedAt: new Date().toISOString(),
|
|
reportDir: path.resolve(reportDir),
|
|
window: {
|
|
durationDays: 7,
|
|
scope: "small-team-all-employees",
|
|
realBusinessDataAllowed: true,
|
|
fixPolicy: "blocking_same_day_issue_contract_nonblocking_feedback_pool",
|
|
},
|
|
oaPrimaryDomains: [
|
|
"reimbursement_pdf_ocr",
|
|
"approval_tasks",
|
|
"procurement_expense",
|
|
"contracts_registry",
|
|
"projects",
|
|
"payment_batches",
|
|
"bank_receipt_attachments",
|
|
"ap_accounting_handoff",
|
|
"mobile_wecom_entry",
|
|
"audit",
|
|
],
|
|
frozenItems: [
|
|
"image_ocr",
|
|
"complex_bpm",
|
|
"budget_control",
|
|
"erp_write",
|
|
"bank_statement_import",
|
|
"real_payment_provider",
|
|
"wecom_hr_deep_mirror",
|
|
"emergency_takeover",
|
|
"recovery_writeback",
|
|
"ruleops",
|
|
],
|
|
boundaries: {
|
|
defaultReadOnlyAutomation: true,
|
|
manualRealBusinessWritesOnly: true,
|
|
paymentBatchExpected: "force_on",
|
|
erpExpected: "force_off",
|
|
actualPaymentChannel: "offline_bank_channel",
|
|
contractSensitiveFiles: "do_not_upload_confidential_body_sensitive_quote_cost_detail_to_public_oa",
|
|
},
|
|
childReports: {
|
|
liveReadiness: child("live-smoke-readiness"),
|
|
},
|
|
steps,
|
|
readonlyProbe,
|
|
findingCount: findings.length,
|
|
findings,
|
|
manualGates: {
|
|
dailyCloseout: "管理员每日查看 summary.json 和 manual-gates.md,阻断即修,非阻断入反馈池。",
|
|
day7Closeout: [
|
|
"A:接受 PASS_OA_REAL_USE_READY,进入下一轮体验/效率优化。",
|
|
"B:接受 PASS_WITH_FINDINGS,但先处理指定 finding。",
|
|
"C:暂停真实使用扩展,先做整改。",
|
|
],
|
|
},
|
|
privacy: "status/count/report-path summaries only; no phone/email/token/Secret/EncodingAESKey/full external ids/full invoice image/full bank receipt/full contract body/raw payload",
|
|
};
|
|
|
|
fs.writeFileSync(path.join(reportDir, "summary.json"), `${JSON.stringify(summary, null, 2)}\n`, "utf8");
|
|
console.log(`[oaops1] summary=${path.join(reportDir, "summary.json")}`);
|
|
NODE
|
|
}
|
|
|
|
finish() {
|
|
write_manual_checklist
|
|
write_summary_json || STATUS=1
|
|
run_privacy_smoke
|
|
log "[oaops1] report=$REPORT_DIR"
|
|
exit "$STATUS"
|
|
}
|
|
|
|
trap finish EXIT
|
|
|
|
load_default_env
|
|
run_ready_and_runtime
|
|
cooldown_login_step "authenticated-readonly-probe"
|
|
run_finding_step "authenticated-readonly-probe" run_authenticated_readonly_probe
|
|
run_mobile_and_entry_matrix
|