filegrc 0.5.0 → 0.6.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
@@ -20,6 +20,7 @@ import {
20
20
  } from "./obligations.js";
21
21
  import { assessAuditPreparation } from "./audit-preparation.js";
22
22
  import { assessProgramReadiness } from "./program-readiness.js";
23
+ import { resolveProgram } from "./program.js";
23
24
  import { planReconciliation } from "./reconciliation.js";
24
25
  import { currentCalendarDate } from "./time.js";
25
26
  import { validateWorkspace } from "./validate.js";
@@ -81,14 +82,22 @@ export async function assessWorkflow(input, options = {}) {
81
82
  : await loadWorkspace(input);
82
83
  const workspace = loaded.workspace
83
84
  || loaded.resources.find((record) => record.type === "workspace");
85
+ const selectedAuditRecords = selectedAudits(loaded.resources, options.auditId);
86
+ const programRecord = resolveProgram(loaded, options.programId || (options.auditId ? selectedAuditRecords[0]?.programId : undefined));
87
+ if (options.auditId && selectedAuditRecords[0]?.programId && selectedAuditRecords[0].programId !== programRecord.id) {
88
+ throw new Error(`Audit "${options.auditId}" belongs to Program "${selectedAuditRecords[0].programId}", not "${programRecord.id}".`);
89
+ }
90
+ const audits = String(loaded.model.modelVersion) === "4"
91
+ ? selectedAuditRecords.filter(({ programId }) => programId === programRecord.id)
92
+ : selectedAuditRecords;
84
93
  const timezone = options.timezone || workspace?.timezone || "UTC";
85
94
  const asOf = options.asOf || currentCalendarDate(timezone);
86
95
  const evaluatedAt = options.evaluatedAt || options.now || new Date().toISOString();
87
96
  const program = options.programReadiness || await assessProgramReadiness(loaded, {
88
97
  asOf,
89
- generatedAt: evaluatedAt
98
+ generatedAt: evaluatedAt,
99
+ programId: programRecord.type === "program" ? programRecord.id : undefined
90
100
  });
91
- const audits = selectedAudits(loaded.resources, options.auditId);
92
101
  const auditPreparations = options.auditPreparations || Object.fromEntries(await Promise.all(
93
102
  audits.map(async (audit) => [
94
103
  audit.id,
@@ -111,26 +120,27 @@ export async function assessWorkflow(input, options = {}) {
111
120
  const coverage = normalizeCoverage(
112
121
  options.coverage
113
122
  || (options.auditId ? audits[0]?.coverage : null)
114
- || workspace?.candidateCoverage
123
+ || programRecord?.candidateCoverage
115
124
  );
116
125
  const periodFindings = await assessPeriodHealth(loaded, {
117
126
  coverage,
118
127
  asOf,
119
128
  evaluatedAt,
120
- controlIds: options.auditId ? audits[0]?.controlIds : workspace?.controlIds
129
+ programId: programRecord.type === "program" ? programRecord.id : undefined,
130
+ controlIds: options.auditId ? audits[0]?.controlIds : programRecord?.controlIds
121
131
  });
122
132
  const guidedFindings = [
123
133
  ...programFindings(program),
124
134
  ...Object.values(auditPreparations).flatMap(auditFindings),
125
135
  ...appointmentTemplateFindings(loaded),
126
- ...sourceCoverageFindings(loaded),
136
+ ...sourceCoverageFindings(loaded, programRecord),
127
137
  ...reconciliationFindings(reconciliation),
128
138
  ...periodFindings,
129
- ...auditLifecycleFindings(loaded, audits)
139
+ ...auditLifecycleFindings(loaded, audits, programRecord)
130
140
  ];
131
141
  const findings = [
132
142
  ...validationFindings(validation, loaded),
133
- ...removeRedundantRecordFindings(recordFinalizationFindings(loaded), guidedFindings),
143
+ ...removeRedundantRecordFindings(recordFinalizationFindings(loaded, programRecord), guidedFindings),
134
144
  ...guidedFindings
135
145
  ];
136
146
  const workItems = buildWorkItems(loaded.resources, obligationPlan, {
@@ -329,11 +339,11 @@ function validationFindings(validation, loaded) {
329
339
  });
330
340
  }
331
341
 
332
- function recordFinalizationFindings(loaded) {
342
+ function recordFinalizationFindings(loaded, program) {
333
343
  const findings = [];
334
344
  for (const record of loaded.resources) {
335
345
  if (["workspace", "renderer-settings"].includes(record.type)) continue;
336
- const incomplete = recordIncompleteReason(record, loaded);
346
+ const incomplete = recordIncompleteReason(record, loaded, program);
337
347
  if (incomplete) {
338
348
  findings.push(finalizationFinding(record, incomplete));
339
349
  }
@@ -389,7 +399,7 @@ function subjectKey(subject) {
389
399
  return `${subject.type}:${subject.id}`;
390
400
  }
391
401
 
392
- function recordIncompleteReason(record, loaded) {
402
+ function recordIncompleteReason(record, loaded, program) {
393
403
  if (record.type === "requirement" && record.applicability === "undetermined") {
394
404
  return {
395
405
  state: "ready",
@@ -418,7 +428,7 @@ function recordIncompleteReason(record, loaded) {
418
428
  "partially-implemented": "Finish the remaining control design, operation, source, and scheduling work."
419
429
  };
420
430
  if (!messages[record.status]) return null;
421
- const requiredness = recordFinalizationRequiredness(record, loaded);
431
+ const requiredness = recordFinalizationRequiredness(record, loaded, program);
422
432
  if (record.status === "blocked") {
423
433
  const byId = new Map(loaded.resources.map((resource) => [resource.id, resource]));
424
434
  return {
@@ -437,11 +447,12 @@ function recordIncompleteReason(record, loaded) {
437
447
  };
438
448
  }
439
449
 
440
- function recordFinalizationRequiredness(record, loaded) {
450
+ function recordFinalizationRequiredness(record, loaded, program) {
441
451
  if (!["policy", "document", "training"].includes(record.type)) return "required";
452
+ const target = program || resolveProgram(loaded);
442
453
  const selectedControlIds = new Set(
443
- loaded.workspace?.controlIds?.length
444
- ? loaded.workspace.controlIds
454
+ target?.controlIds?.length
455
+ ? target.controlIds
445
456
  : loaded.resources
446
457
  .filter((resource) => resource.type === "control" && !["not-applicable", "retired"].includes(resource.status))
447
458
  .map(({ id }) => id)
@@ -606,11 +617,19 @@ function appointmentFinding(kind, template, record, state, requiredness) {
606
617
  };
607
618
  }
608
619
 
609
- function sourceCoverageFindings(loaded) {
620
+ function sourceCoverageFindings(loaded, program) {
621
+ const selected = String(loaded.model.modelVersion) === "4"
622
+ ? new Set(program?.controlIds || [])
623
+ : null;
610
624
  const selectedControlIds = loaded.resources
611
- .filter((record) => record.type === "control" && record.status !== "not-applicable" && record.status !== "retired")
625
+ .filter((record) => (
626
+ record.type === "control"
627
+ && record.status !== "not-applicable"
628
+ && record.status !== "retired"
629
+ && (!selected || selected.has(record.id))
630
+ ))
612
631
  .map(({ id }) => id);
613
- return assessSourceCoverageReadiness(loaded, selectedControlIds)
632
+ return assessSourceCoverageReadiness(loaded, selectedControlIds, program)
614
633
  .map(({ family, record, complete }) => {
615
634
  const state = complete ? "complete" : "ready";
616
635
  const code = `evidence-source.${family.id}.coverage`;
@@ -659,15 +678,16 @@ function reconciliationFindings(reconciliation) {
659
678
 
660
679
  async function assessPeriodHealth(loaded, options) {
661
680
  const coverage = options.coverage;
681
+ const target = resolveProgram(loaded, options.programId);
662
682
  if (!coverage?.start || !coverage?.end) {
663
- if (loaded.workspace?.assuranceGoal !== "soc-2-type-2") return [];
683
+ if (target?.assuranceGoal !== "soc-2-type-2") return [];
664
684
  return [periodFinding(
665
685
  "period.coverage.select",
666
686
  "Select the candidate operating period",
667
687
  "Set the candidate Type 2 start and end dates before FileGRC can calculate continuous policy, control, source, role, and obligation coverage.",
668
688
  "ready",
669
- { type: "workspace", id: loaded.workspace.id },
670
- [{ kind: "command", command: "npx filegrc get workspace workspace --mutation" }]
689
+ { type: target.type, id: target.id },
690
+ [{ kind: "command", command: `npx filegrc get ${target.id} --mutation` }]
671
691
  )];
672
692
  }
673
693
 
@@ -680,7 +700,8 @@ async function assessPeriodHealth(loaded, options) {
680
700
  "obligation",
681
701
  "policy",
682
702
  "source-coverage",
683
- "system"
703
+ "system",
704
+ "component"
684
705
  ].includes(record.type));
685
706
  const paths = relevantEntries.map(({ relativePath }) => `data/${relativePath}`);
686
707
  const histories = getWorkspaceHistories(loaded.root, paths, 50);
@@ -843,8 +864,9 @@ async function assessPeriodHealth(loaded, options) {
843
864
  return findings;
844
865
  }
845
866
 
846
- function auditLifecycleFindings(loaded, audits) {
847
- if (String(loaded.model.modelVersion) !== "3") return [];
867
+ function auditLifecycleFindings(loaded, audits, program) {
868
+ if (!["3", "4"].includes(String(loaded.model.modelVersion))) return [];
869
+ const target = program || resolveProgram(loaded);
848
870
  const findings = [];
849
871
  for (const audit of audits) {
850
872
  if (!(audit.controlIds || []).length) {
@@ -857,7 +879,7 @@ function auditLifecycleFindings(loaded, audits) {
857
879
  ));
858
880
  }
859
881
  if (!audit.scopeRevision) {
860
- const workspaceControls = new Set(loaded.workspace?.controlIds || []);
882
+ const workspaceControls = new Set(target?.controlIds || []);
861
883
  const omittedControls = [...workspaceControls].filter((id) => !(audit.controlIds || []).includes(id));
862
884
  findings.push(auditLifecycleFinding(
863
885
  audit,
@@ -879,7 +901,7 @@ function auditLifecycleFindings(loaded, audits) {
879
901
  "Set the exact start and end dates before evaluating expected occurrences and source continuity."
880
902
  ));
881
903
  }
882
- const candidateStart = coverageStart(loaded.workspace?.candidateCoverage);
904
+ const candidateStart = coverageStart(target?.candidateCoverage);
883
905
  if (start && candidateStart && start < candidateStart) {
884
906
  findings.push(auditLifecycleFinding(
885
907
  audit,