filegrc 0.9.2 → 0.11.0

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
@@ -2,6 +2,7 @@ import { cp, mkdtemp, rm } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { modelSupports } from "../model/index.js";
5
+ import { applicabilityReviewIsCurrent } from "./applicability-scope.js";
5
6
  import { assessRequiredAppointments } from "./appointments.js";
6
7
  import { assessSourceCoverageReadiness } from "./source-coverage.js";
7
8
  import { coverageEnd, coverageStart } from "./coverage.js";
@@ -22,6 +23,7 @@ import {
22
23
  import { assessAuditPreparation } from "./audit-preparation.js";
23
24
  import { assessProgramReadiness } from "./program-readiness.js";
24
25
  import { resolveProgram } from "./program.js";
26
+ import { resourceReviewRevisions, retentionReviewResourceIds, retentionRuleIsCurrent } from "./retention.js";
25
27
  import { signatoryAppointmentIssue, soc2ReportEvidenceIssue, subsequentEventsReviewIssue } from "./soc2.js";
26
28
  import { planReconciliation } from "./reconciliation.js";
27
29
  import { currentCalendarDate } from "./time.js";
@@ -110,8 +112,8 @@ async function assessWorkflowUnmeasured(input, options = {}) {
110
112
  audit.id,
111
113
  await assessAuditPreparation(loaded, {
112
114
  auditId: audit.id,
113
- generatedAt: evaluatedAt,
114
- programReadiness: program
115
+ asOf,
116
+ generatedAt: evaluatedAt
115
117
  })
116
118
  ])
117
119
  ));
@@ -140,10 +142,10 @@ async function assessWorkflowUnmeasured(input, options = {}) {
140
142
  ...programFindings(program),
141
143
  ...Object.values(auditPreparations).flatMap(auditFindings),
142
144
  ...appointmentTemplateFindings(loaded),
143
- ...sourceCoverageFindings(loaded, programRecord),
145
+ ...await sourceCoverageFindings(loaded, programRecord),
144
146
  ...reconciliationFindings(reconciliation),
145
147
  ...periodFindings,
146
- ...auditLifecycleFindings(loaded, audits, programRecord)
148
+ ...await auditLifecycleFindings(loaded, audits, programRecord)
147
149
  ];
148
150
  const findings = [
149
151
  ...validationFindings(validation, loaded),
@@ -363,7 +365,7 @@ function recordFinalizationFindings(loaded, program) {
363
365
  if (incomplete) {
364
366
  findings.push(finalizationFinding(record, incomplete));
365
367
  }
366
- for (const missing of finalizationFields(record, loaded.model)) {
368
+ for (const missing of finalizationFields(record, loaded.model, loaded.resources, program)) {
367
369
  findings.push(fieldFinding(record, missing));
368
370
  }
369
371
  }
@@ -498,16 +500,16 @@ function recordFinalizationRequiredness(record, loaded, program) {
498
500
  return "conditional";
499
501
  }
500
502
 
501
- function finalizationFields(record, model) {
503
+ function finalizationFields(record, model, resources, program) {
502
504
  const fields = [];
503
505
  const needsReview = ["requirement", "commitment", "complementary-control", "control"].includes(record.type)
504
506
  && model.resources[record.type]?.fields?.applicabilityReview
505
- && !record.applicabilityReview;
507
+ && !applicabilityReviewIsCurrent(record.applicabilityReview, record, program, resources, model);
506
508
  if (needsReview) {
507
509
  fields.push({
508
510
  field: "applicabilityReview",
509
511
  requiredness: "required",
510
- message: "Record the reviewed applicability decision, rationale, reviewer, and date. FileGRC records the current scope automatically."
512
+ message: "Record or refresh the reviewed applicability decision, rationale, reviewer, and date. FileGRC records the current material scope automatically."
511
513
  });
512
514
  }
513
515
  if (record.type === "policy" && model.resources.policy?.fields?.programRole && !record.programRole) {
@@ -637,7 +639,7 @@ function appointmentFinding(kind, template, record, state, requiredness) {
637
639
  };
638
640
  }
639
641
 
640
- function sourceCoverageFindings(loaded, program) {
642
+ async function sourceCoverageFindings(loaded, program) {
641
643
  const selected = modelSupports(loaded.model, "program-scope")
642
644
  ? new Set(program?.controlIds || [])
643
645
  : null;
@@ -649,7 +651,7 @@ function sourceCoverageFindings(loaded, program) {
649
651
  && (!selected || selected.has(record.id))
650
652
  ))
651
653
  .map(({ id }) => id);
652
- return assessSourceCoverageReadiness(loaded, selectedControlIds, program)
654
+ return (await assessSourceCoverageReadiness(loaded, selectedControlIds, program))
653
655
  .map(({ family, record, complete }) => {
654
656
  const state = complete ? "complete" : "ready";
655
657
  const code = `evidence-source.${family.id}.coverage`;
@@ -884,10 +886,12 @@ async function assessPeriodHealth(loaded, options) {
884
886
  return findings;
885
887
  }
886
888
 
887
- function auditLifecycleFindings(loaded, audits, program) {
889
+ async function auditLifecycleFindings(loaded, audits, program) {
888
890
  if (!modelSupports(loaded.model, "guided-workflow")) return [];
889
891
  const target = program || resolveProgram(loaded);
890
892
  const byId = new Map(loaded.resources.map((record) => [record.id, record]));
893
+ const retentionRules = loaded.resources.filter((record) => record.type === "retention-schedule-item");
894
+ const retentionRevisions = await resourceReviewRevisions(loaded, retentionRules.flatMap((rule) => retentionReviewResourceIds(rule, loaded)));
891
895
  const findings = [];
892
896
  for (const audit of audits) {
893
897
  if (!(audit.controlIds || []).length) {
@@ -1015,11 +1019,21 @@ function auditLifecycleFindings(loaded, audits, program) {
1015
1019
  `${records.length} ${noun} remain open, so this engagement cannot be treated as closed.`
1016
1020
  ));
1017
1021
  }
1022
+ const linkedRetentionCurrent = (audit.retentionScheduleItemIds || []).some((id) => {
1023
+ const rule = byId.get(id);
1024
+ return rule?.type === "retention-schedule-item"
1025
+ && rule.status === "active"
1026
+ && (rule.scopeResourceIds || []).some((scopeId) => scopeId === audit.id || scopeId === target?.id)
1027
+ && retentionRuleIsCurrent(rule, retentionRevisions, byId, loaded);
1028
+ });
1029
+ const retentionClosure = loaded.model.resources.audit?.fields?.retentionScheduleItemIds
1030
+ ? ["retentionScheduleItemIds", "Link the audit retention schedule", "Link an active, current retention schedule item scoped to this Audit or its Program."]
1031
+ : ["retentionDecision", "Record the retention decision", "Record the approved retention and authorized distribution decision."];
1018
1032
  for (const [field, title, message] of [
1019
- ["retentionDecision", "Record the retention decision", "Record the approved retention and authorized distribution decision."],
1033
+ retentionClosure,
1020
1034
  ["carryForwardActionIds", "Review carry-forward work", "Record the next-period actions, including an explicit empty list when no carry-forward work remains."]
1021
1035
  ]) {
1022
- if (field === "carryForwardActionIds" ? Array.isArray(audit[field]) : present(audit[field])) continue;
1036
+ if (field === "retentionScheduleItemIds" ? linkedRetentionCurrent : field === "carryForwardActionIds" ? Array.isArray(audit[field]) : present(audit[field])) continue;
1023
1037
  findings.push(auditLifecycleFinding(audit, field, "audit-closure", title, message));
1024
1038
  }
1025
1039
  }
@@ -1081,6 +1095,12 @@ function normalizeFinding(code, item, context) {
1081
1095
  severity: findingSeverity(state, context.assessment),
1082
1096
  title: item.title,
1083
1097
  message: item.message,
1098
+ ...(item.resourceId ? { resourceId: item.resourceId } : {}),
1099
+ ...(item.createResourceType ? { createResourceType: item.createResourceType } : {}),
1100
+ ...(item.sourceResourceIds ? { sourceResourceIds: item.sourceResourceIds } : {}),
1101
+ ...(item.informationTypeId ? { informationTypeId: item.informationTypeId } : {}),
1102
+ ...(item.retentionScheduleItemIds ? { retentionScheduleItemIds: item.retentionScheduleItemIds } : {}),
1103
+ ...(item.staleResourceIds ? { staleResourceIds: item.staleResourceIds } : {}),
1084
1104
  ...(subject ? { subject } : {}),
1085
1105
  dependencies,
1086
1106
  actions: (item.commands || []).map((command) => ({