filegrc 0.6.5 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/workflow.js CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  import { assessAuditPreparation } from "./audit-preparation.js";
22
22
  import { assessProgramReadiness } from "./program-readiness.js";
23
23
  import { resolveProgram } from "./program.js";
24
+ import { signatoryAppointmentIssue, soc2ReportEvidenceIssue, subsequentEventsReviewIssue } from "./soc2.js";
24
25
  import { planReconciliation } from "./reconciliation.js";
25
26
  import { currentCalendarDate } from "./time.js";
26
27
  import { measureTiming } from "./timing.js";
@@ -304,15 +305,24 @@ function programFindings(program) {
304
305
  }
305
306
 
306
307
  function auditFindings(preparation) {
307
- return preparation.stages.flatMap((stage) => stage.items.map((item) => normalizeFinding(
308
- `audit.${preparation.audit?.id || "unscoped"}.${stage.id}.${item.id}`,
309
- item,
310
- {
311
- assessment: stage.id === "auditor" ? "audit-closure" : "audit-readiness",
312
- stage: stage.id,
313
- auditId: preparation.audit?.id || null
314
- }
315
- )));
308
+ return preparation.stages.flatMap((stage) => stage.items.map((item) => {
309
+ const finding = normalizeFinding(
310
+ `audit.${preparation.audit?.id || "unscoped"}.${stage.id}.${item.id}`,
311
+ item,
312
+ {
313
+ assessment: stage.id === "auditor" ? "audit-closure" : "audit-readiness",
314
+ stage: stage.id,
315
+ auditId: preparation.audit?.id || null
316
+ }
317
+ );
318
+ if (
319
+ finding.state === "ready"
320
+ && !finding.actions.length
321
+ && finding.subject?.type === "audit"
322
+ && preparation.audit
323
+ ) finding.actions = [mutationAction(preparation.audit)];
324
+ return finding;
325
+ }));
316
326
  }
317
327
 
318
328
  function validationFindings(validation, loaded) {
@@ -872,6 +882,7 @@ async function assessPeriodHealth(loaded, options) {
872
882
  function auditLifecycleFindings(loaded, audits, program) {
873
883
  if (!["3", "4"].includes(String(loaded.model.modelVersion))) return [];
874
884
  const target = program || resolveProgram(loaded);
885
+ const byId = new Map(loaded.resources.map((record) => [record.id, record]));
875
886
  const findings = [];
876
887
  for (const audit of audits) {
877
888
  if (!(audit.controlIds || []).length) {
@@ -917,45 +928,54 @@ function auditLifecycleFindings(loaded, audits, program) {
917
928
  ));
918
929
  }
919
930
  }
920
- if (!["planning", "draft"].includes(audit.status)) {
921
- if (!audit.engagementTermsDocumentId) {
922
- findings.push(auditLifecycleFinding(
923
- audit,
924
- "engagement-terms",
925
- "audit-readiness",
926
- "Link the engagement terms",
927
- "Record the CPA firm's engagement terms without representing its professional judgments as management facts."
928
- ));
929
- }
930
- if (!(audit.managementAcknowledgedByIds || []).length || !audit.managementAcknowledgedOn) {
931
- findings.push(auditLifecycleFinding(
932
- audit,
933
- "management-acknowledgement",
934
- "audit-readiness",
935
- "Record management acknowledgement",
936
- "Name who acknowledged the engagement terms and the actual acknowledgement date."
937
- ));
938
- }
939
- }
940
931
  const lateStage = ["report-draft", "issued", "delivered", "complete"].includes(audit.status);
941
- if (lateStage && !audit.subsequentEventsReview) {
932
+ const subsequentEventsIssue = lateStage ? subsequentEventsReviewIssue(audit) : null;
933
+ if (subsequentEventsIssue) {
942
934
  findings.push(auditLifecycleFinding(
943
935
  audit,
944
936
  "subsequent-events",
945
937
  "audit-readiness",
946
938
  "Complete the subsequent-events review",
947
- "Review incidents, changes, findings, subservice coverage, representations, and system-description disclosures through the report date."
939
+ subsequentEventsIssue.message
948
940
  ));
949
941
  }
950
- if (["fieldwork", "report-draft", "issued", "delivered", "complete"].includes(audit.status) && !audit.packetDelivery) {
942
+ const signatoryIssue = String(loaded.model.modelVersion) === "4" && lateStage
943
+ ? signatoryAppointmentIssue(audit, byId)
944
+ : null;
945
+ if (signatoryIssue) {
946
+ findings.push(auditLifecycleFinding(
947
+ audit,
948
+ "signatory-authority",
949
+ "audit-readiness",
950
+ "Confirm authorized signatories",
951
+ signatoryIssue.message
952
+ ));
953
+ }
954
+ const packetIssue = ["fieldwork", "report-draft", "issued", "delivered", "complete"].includes(audit.status)
955
+ ? packetDeliveryIssue(audit.packetDelivery)
956
+ : null;
957
+ if (packetIssue) {
951
958
  findings.push(auditLifecycleFinding(
952
959
  audit,
953
960
  "packet-delivery",
954
961
  "delivery-readiness",
955
962
  "Approve and record packet delivery",
956
- "Record the least-disclosure review, redaction decision, recipient, delivery system, exact packet revision and manifest, management approval, delivery date, and receipt."
963
+ packetIssue
957
964
  ));
958
965
  }
966
+ if (["issued", "delivered", "complete"].includes(audit.status)) {
967
+ const reportEvidence = loaded.resources.find((record) => record.id === audit.reportEvidenceId);
968
+ const reportIssue = soc2ReportEvidenceIssue(reportEvidence, audit, loaded.model.modelVersion);
969
+ if (reportIssue) {
970
+ findings.push(auditLifecycleFinding(
971
+ audit,
972
+ "report-evidence",
973
+ "audit-closure",
974
+ "Link the exact issued SOC 2 report",
975
+ reportIssue.message
976
+ ));
977
+ }
978
+ }
959
979
  if (audit.status !== "complete") {
960
980
  const nextStep = auditClosureNextStep(audit.status);
961
981
  findings.push(auditLifecycleFinding(
@@ -991,10 +1011,8 @@ function auditLifecycleFindings(loaded, audits, program) {
991
1011
  ));
992
1012
  }
993
1013
  for (const [field, title, message] of [
994
- ["reportEvidenceId", "Link the issued report", "Link the exact issued report evidence and its coverage."],
995
1014
  ["retentionDecision", "Record the retention decision", "Record the approved retention and authorized distribution decision."],
996
- ["carryForwardActionIds", "Review carry-forward work", "Record the next-period actions, including an explicit empty list when no carry-forward work remains."],
997
- ["signatoryAppointmentIds", "Confirm authorized signatories", "Link active authority Appointments for management assertion and representation signers."]
1015
+ ["carryForwardActionIds", "Review carry-forward work", "Record the next-period actions, including an explicit empty list when no carry-forward work remains."]
998
1016
  ]) {
999
1017
  if (field === "carryForwardActionIds" ? Array.isArray(audit[field]) : present(audit[field])) continue;
1000
1018
  findings.push(auditLifecycleFinding(audit, field, "audit-closure", title, message));
@@ -1003,6 +1021,41 @@ function auditLifecycleFindings(loaded, audits, program) {
1003
1021
  return findings;
1004
1022
  }
1005
1023
 
1024
+ export function packetDeliveryIssue(delivery) {
1025
+ if (!delivery) {
1026
+ return "Record the least-disclosure review, redaction decision, recipient, delivery system, exact packet revision and manifest, management approval, delivery date, and receipt.";
1027
+ }
1028
+ if (!(delivery.classificationReviewedByIds || []).length || !(delivery.approvedByIds || []).length) {
1029
+ return "Name the people who performed the least-disclosure review and approved the delivery.";
1030
+ }
1031
+ for (const [field, label] of [
1032
+ ["redactionDecision", "redaction decision"],
1033
+ ["recipient", "recipient"],
1034
+ ["deliverySystem", "delivery system"],
1035
+ ["packetCommit", "packet Git revision"],
1036
+ ["manifestChecksum", "manifest checksum"],
1037
+ ["receiptReference", "delivery receipt reference"]
1038
+ ]) {
1039
+ if (!String(delivery[field] || "").trim()) return `Record the ${label} for the delivered packet.`;
1040
+ }
1041
+ if (!/^[a-f0-9]{40}$/i.test(delivery.packetCommit)) {
1042
+ return "Record the delivered packet's exact 40-character Git commit.";
1043
+ }
1044
+ if (!/^(?:sha256:)?[a-f0-9]{64}$/i.test(delivery.manifestChecksum)) {
1045
+ return "Record the packet manifest checksum as a SHA-256 digest.";
1046
+ }
1047
+ if (
1048
+ !delivery.classificationReviewedOn
1049
+ || !delivery.approvedOn
1050
+ || !delivery.deliveredOn
1051
+ || delivery.classificationReviewedOn > delivery.approvedOn
1052
+ || delivery.approvedOn > delivery.deliveredOn
1053
+ ) {
1054
+ return "Record chronological least-disclosure review, management approval, and delivery dates.";
1055
+ }
1056
+ return null;
1057
+ }
1058
+
1006
1059
  function normalizeFinding(code, item, context) {
1007
1060
  const state = findingState(item.status);
1008
1061
  const subject = item.resourceType
@@ -1185,6 +1238,9 @@ function buildAssessments({ program, audits, auditPreparations, obligationPlan,
1185
1238
  && audits.every((audit) => audit.status === "complete")
1186
1239
  && !findings.some((finding) => finding.assessment === "audit-closure" && blockingFinding(finding));
1187
1240
  const deliveryFindingKeys = findingKeys(findings, "delivery-readiness");
1241
+ const policyActivations = program.policyActivations || [];
1242
+ const policiesOperating = policyActivations.length > 0
1243
+ && policyActivations.every(({ state }) => state === "active-and-operating");
1188
1244
  return {
1189
1245
  structuralValidity: assessment(
1190
1246
  validation.ok ? "complete" : "needs-work",
@@ -1203,6 +1259,24 @@ function buildAssessments({ program, audits, auditPreparations, obligationPlan,
1203
1259
  evidenceReady ? "Evidence collection can begin." : "Evidence collection prerequisites remain.",
1204
1260
  findingKeys(findings, "program-configuration", ({ key }) => key.startsWith("program."))
1205
1261
  ),
1262
+ policyActivation: {
1263
+ status: policiesOperating ? "complete" : policyActivations.length ? "needs-work" : "not-started",
1264
+ message: policiesOperating
1265
+ ? "Every required Policy is active and operating."
1266
+ : policyActivations.length
1267
+ ? "Review the per-Policy implementation gaps, select the approved Policies, and confirm the Step 3 activation cutover."
1268
+ : "Approve the required Policies before activation assessment begins.",
1269
+ findingKeys: findingKeys(findings, "program-configuration", ({ key }) => key.includes(".policy-activation-")),
1270
+ policies: policyActivations
1271
+ },
1272
+ policyLibraryReview: {
1273
+ status: program.policyLibraryProposals?.length ? "review" : "current",
1274
+ message: program.policyLibraryProposals?.length
1275
+ ? "Optional starter-library review proposals are available. Existing content is unchanged."
1276
+ : "No starter-library review proposal applies.",
1277
+ findingKeys: [],
1278
+ proposals: program.policyLibraryProposals || []
1279
+ },
1206
1280
  periodHealth: assessment(
1207
1281
  !periodStarted ? "not-started" : periodHealthy ? "complete" : "at-risk",
1208
1282
  !evidenceReady
@@ -1430,14 +1504,10 @@ function periodFinding(key, title, message, state, subject, actions, dependencie
1430
1504
 
1431
1505
  function auditClosureNextStep(status) {
1432
1506
  const steps = {
1433
- planning: {
1507
+ planned: {
1434
1508
  title: "Confirm the engagement and start audit preparation",
1435
1509
  message: "Link the agreed engagement terms, confirm scope and management acknowledgement, then move the audit to in progress."
1436
1510
  },
1437
- draft: {
1438
- title: "Finalize the draft engagement",
1439
- message: "Resolve the draft scope and ownership, link the agreed engagement terms, then move the audit to in progress."
1440
- },
1441
1511
  "in-progress": {
1442
1512
  title: "Begin fieldwork",
1443
1513
  message: "Finish management preparation, confirm the fieldwork dates, and move the audit to fieldwork when the CPA firm begins testing."