filegrc 0.7.1 → 0.9.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/README.md +7 -7
- package/model/index.js +22 -4
- package/model/v5.json +10233 -0
- package/model/v6.json +10358 -0
- package/package.json +4 -2
- package/src/agent.js +4 -3
- package/src/audit-preparation.js +182 -45
- package/src/audit-transition.js +3 -2
- package/src/batch-review.js +7 -6
- package/src/cli.js +143 -12
- package/src/collection-review.js +4 -3
- package/src/collection-scope.js +4 -3
- package/src/document-activation.js +181 -0
- package/src/evidence-packet.js +199 -78
- package/src/external-reviewer.js +5 -4
- package/src/files.js +173 -35
- package/src/git.js +71 -7
- package/src/index.js +11 -1
- package/src/model-migration.js +363 -7
- package/src/obligations.js +14 -11
- package/src/policy-library.js +7 -3
- package/src/program-lifecycle.js +131 -3
- package/src/program-path.js +19 -13
- package/src/program-readiness.js +338 -71
- package/src/program.js +5 -3
- package/src/reconciliation.js +3 -2
- package/src/server.js +36 -7
- package/src/setup.js +8 -5
- package/src/soc2.js +3 -2
- package/src/validate.js +178 -16
- package/src/web.js +264 -17
- package/src/workflow.js +38 -7
- package/src/workspace.js +5 -0
package/src/program-readiness.js
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { modelSupports } from "../model/index.js";
|
|
2
3
|
import { assessRequiredAppointments } from "./appointments.js";
|
|
3
4
|
import { assessCollectionReviews } from "./collection-review.js";
|
|
4
5
|
import { openPlaceholderCount, substantiveMarkdown } from "./content-readiness.js";
|
|
5
6
|
import { coverageEnd, coverageStart } from "./coverage.js";
|
|
6
7
|
import { planObligations } from "./obligations.js";
|
|
7
8
|
import { resolveDataPath } from "./paths.js";
|
|
8
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
contentRevisionBindingsMatch,
|
|
11
|
+
documentIsAuditSpecific,
|
|
12
|
+
governedDocumentIsOperating,
|
|
13
|
+
obligationIsEnabled,
|
|
14
|
+
obligationIsRunning
|
|
15
|
+
} from "./program-lifecycle.js";
|
|
9
16
|
import { currentPartyPeople, partiesIndependent, partyPeople } from "./parties.js";
|
|
10
17
|
import { assessPolicyLibraryUpgrades } from "./policy-library.js";
|
|
11
18
|
import { programComponents, resolveProgram, selectedRequirementIds } from "./program.js";
|
|
12
19
|
import { markdownEntries } from "./resource-markdown.js";
|
|
13
20
|
import {
|
|
14
21
|
missingSoc2References,
|
|
22
|
+
personWasActiveOn,
|
|
15
23
|
REQUIRED_SOC2_DESCRIPTION_REFERENCES,
|
|
16
24
|
REQUIRED_SOC2_SECURITY_REFERENCES
|
|
17
25
|
} from "./soc2.js";
|
|
@@ -37,14 +45,15 @@ export async function assessProgramReadiness(input, options = {}) {
|
|
|
37
45
|
return markdown.get(record.id);
|
|
38
46
|
};
|
|
39
47
|
|
|
40
|
-
const policyStage = await policiesStage(scope, records, byId, readMarkdown);
|
|
48
|
+
const policyStage = await policiesStage(scope, records, byId, readMarkdown, loaded.model);
|
|
41
49
|
const controlStage = await controlsStage(scope, byId, readMarkdown, asOf, loaded.model);
|
|
42
50
|
controlStage.items.unshift(...collectionReviews
|
|
43
51
|
.filter(({ resourceType }) => resourceType === "complementary-control")
|
|
44
52
|
.map(collectionReviewReadinessItem));
|
|
45
53
|
const sourceStage = await evidenceSourcesStage(scope, byId, loaded.model, readMarkdown);
|
|
46
54
|
controlStage.items.push(...sourceStage.items);
|
|
47
|
-
|
|
55
|
+
const governedContent = await governedContentItems(scope, records, byId, readMarkdown, asOf, loaded.model);
|
|
56
|
+
controlStage.items.push(...governedContent.items);
|
|
48
57
|
const policyActivations = await assessPolicyActivations(
|
|
49
58
|
requiredPolicies(scope, byId),
|
|
50
59
|
scope.controls,
|
|
@@ -55,7 +64,7 @@ export async function assessProgramReadiness(input, options = {}) {
|
|
|
55
64
|
loaded.model
|
|
56
65
|
);
|
|
57
66
|
controlStage.items.push(...policyActivations.map(policyActivationItem));
|
|
58
|
-
controlStage.description = `Each implemented Control needs an owner, actual procedure, scope, operation pattern, mappings, an implementation date, enabled
|
|
67
|
+
controlStage.description = `Each implemented Control needs an owner, actual procedure, scope, operation pattern, mappings, an implementation date, enabled Obligations, and complete authoritative source ${modelSupports(loaded.model, "component-sources") ? "Components" : "Systems"}. Activate unchanged approved program Documents and Training after their requirements are implemented, then activate approved Policies at the implementation cutover.`;
|
|
59
68
|
const evidenceGateStages = [
|
|
60
69
|
scopeStage(
|
|
61
70
|
program,
|
|
@@ -110,6 +119,8 @@ export async function assessProgramReadiness(input, options = {}) {
|
|
|
110
119
|
canStartCandidatePeriod,
|
|
111
120
|
suggestedCandidatePeriodStart: canStartCandidatePeriod ? asOf : null,
|
|
112
121
|
policyActivations,
|
|
122
|
+
documentActivations: governedContent.documentActivations,
|
|
123
|
+
trainingActivations: governedContent.trainingActivations,
|
|
113
124
|
policyLibraryProposals: [
|
|
114
125
|
...policyLibrary.proposals,
|
|
115
126
|
...legacyPolicyLibraryProposals(records)
|
|
@@ -145,9 +156,9 @@ export async function assessEvidenceMap(input, options = {}) {
|
|
|
145
156
|
status: counts.action ? "action" : "complete",
|
|
146
157
|
counts,
|
|
147
158
|
workflow: [
|
|
148
|
-
`Choose an existing ${
|
|
149
|
-
`On every source ${
|
|
150
|
-
`Map each selected Control to the authoritative source ${
|
|
159
|
+
`Choose an existing ${modelSupports(readiness.dataModelVersion, "component-sources") ? "Component" : "System"} or create one that is authoritative for each evidence family.`,
|
|
160
|
+
`On every source ${modelSupports(readiness.dataModelVersion, "component-sources") ? "Component" : "System"}, set an evidence source role, name current evidence access owners, and write repeatable retrieval instructions in Record Markdown.`,
|
|
161
|
+
`Map each selected Control to the authoritative source ${modelSupports(readiness.dataModelVersion, "component-sources") ? "Components with evidenceSourceComponentIds" : "Systems with evidenceSourceIds"} that produce its evidence.`,
|
|
151
162
|
"Run program-readiness again and resolve every incomplete source check and control mapping before marking the Controls implemented."
|
|
152
163
|
],
|
|
153
164
|
items: evidenceItems
|
|
@@ -169,7 +180,7 @@ function programScope(program, records, byId, model, loaded) {
|
|
|
169
180
|
)),
|
|
170
181
|
requirements: select(selectedRequirementIds(program, model), "requirement", (record) => (
|
|
171
182
|
record.type === "requirement" && record.applicability === "applicable"
|
|
172
|
-
)).filter((record) =>
|
|
183
|
+
)).filter((record) => modelSupports(model, "program-scope") || record.applicability === "applicable"),
|
|
173
184
|
controls: select(program?.controlIds, "control", (record) => (
|
|
174
185
|
record.type === "control" && !["not-applicable", "retired"].includes(record.status)
|
|
175
186
|
)).filter((record) => !["not-applicable", "retired"].includes(record.status))
|
|
@@ -195,7 +206,7 @@ function scopeStage(workspace, scope, records, byId, model, collectionReviews =
|
|
|
195
206
|
}
|
|
196
207
|
));
|
|
197
208
|
|
|
198
|
-
if (
|
|
209
|
+
if (modelSupports(model, "program-scope")) {
|
|
199
210
|
const completeComponents = scope.components.filter((component) => (
|
|
200
211
|
component.status === "active"
|
|
201
212
|
&& component.description
|
|
@@ -226,8 +237,8 @@ function scopeStage(workspace, scope, records, byId, model, collectionReviews =
|
|
|
226
237
|
|
|
227
238
|
const completeSystems = scope.systems.filter((system) => (
|
|
228
239
|
system.status === "active"
|
|
229
|
-
&& (
|
|
230
|
-
&& (
|
|
240
|
+
&& (modelSupports(model, "program-scope") ? system.purpose && system.boundary && (system.servicesProvided || []).length : system.description)
|
|
241
|
+
&& (modelSupports(model, "program-scope") || system.classificationId)
|
|
231
242
|
&& (system.ownerIds || []).length
|
|
232
243
|
));
|
|
233
244
|
items.push(item(
|
|
@@ -235,12 +246,12 @@ function scopeStage(workspace, scope, records, byId, model, collectionReviews =
|
|
|
235
246
|
scope.systems.length && completeSystems.length === scope.systems.length ? "complete" : "action",
|
|
236
247
|
"Define the service boundary",
|
|
237
248
|
scope.systems.length
|
|
238
|
-
? `${completeSystems.length} of ${scope.systems.length} program systems are active, explicitly in scope, owned, and described${
|
|
249
|
+
? `${completeSystems.length} of ${scope.systems.length} program systems are active, explicitly in scope, owned, and described${modelSupports(model, "program-scope") ? "" : ", with a classification"}.`
|
|
239
250
|
: "Select and describe every service and supporting system in the program boundary.",
|
|
240
251
|
scope.systems[0] || { type: "system" }
|
|
241
252
|
));
|
|
242
253
|
|
|
243
|
-
if (
|
|
254
|
+
if (modelSupports(model, "guided-workflow")) {
|
|
244
255
|
const commitments = records.filter((record) => (
|
|
245
256
|
record.type === "commitment"
|
|
246
257
|
&& !["superseded", "retired"].includes(record.status)
|
|
@@ -250,7 +261,7 @@ function scopeStage(workspace, scope, records, byId, model, collectionReviews =
|
|
|
250
261
|
record.status === "active"
|
|
251
262
|
&& record.statement
|
|
252
263
|
&& record.effectiveOn
|
|
253
|
-
&& (
|
|
264
|
+
&& (modelSupports(model, "program-scope") || record.applicabilityReview?.decision === "applicable")
|
|
254
265
|
&& currentPartyPeople(record.ownerIds, byId).size > 0
|
|
255
266
|
&& (record.requirementIds || []).length > 0
|
|
256
267
|
&& (record.controlIds || []).length > 0
|
|
@@ -283,12 +294,12 @@ function scopeStage(workspace, scope, records, byId, model, collectionReviews =
|
|
|
283
294
|
const applicableRequirements = records.filter((record) => (
|
|
284
295
|
record.type === "requirement"
|
|
285
296
|
&& scope.frameworks.some((framework) => framework.id === record.frameworkId)
|
|
286
|
-
&& (
|
|
297
|
+
&& (modelSupports(model, "program-scope") ? v4Decisions.get(record.id) === "applicable" : record.applicability === "applicable")
|
|
287
298
|
));
|
|
288
299
|
const unresolvedRequirements = records.filter((record) => (
|
|
289
300
|
record.type === "requirement"
|
|
290
301
|
&& scope.frameworks.some((framework) => framework.id === record.frameworkId)
|
|
291
|
-
&& (
|
|
302
|
+
&& (modelSupports(model, "program-scope") ? !v4Decisions.has(record.id) || v4Decisions.get(record.id) === "undetermined" : record.applicability === "undetermined")
|
|
292
303
|
));
|
|
293
304
|
const missingRequirements = applicableRequirements.filter((record) => !selectedRequirementIds.has(record.id));
|
|
294
305
|
const selectedDescriptionRequirements = scope.requirements.filter(isDescriptionRequirement);
|
|
@@ -299,7 +310,7 @@ function scopeStage(workspace, scope, records, byId, model, collectionReviews =
|
|
|
299
310
|
!isDescriptionRequirement(requirement)
|
|
300
311
|
&& !scope.controls.some((control) => (control.requirementIds || []).includes(requirement.id))
|
|
301
312
|
));
|
|
302
|
-
const enforceSoc2Baseline =
|
|
313
|
+
const enforceSoc2Baseline = modelSupports(model, "program-scope")
|
|
303
314
|
&& ["readiness", "soc-2-type-1", "soc-2-type-2"].includes(goal);
|
|
304
315
|
const selectedFrameworkRequirements = records.filter((record) => (
|
|
305
316
|
record.type === "requirement"
|
|
@@ -523,15 +534,22 @@ function ownershipResolutionReasons(ownerIds, byId) {
|
|
|
523
534
|
});
|
|
524
535
|
}
|
|
525
536
|
|
|
526
|
-
async function policiesStage(scope, records, byId, readMarkdown) {
|
|
537
|
+
async function policiesStage(scope, records, byId, readMarkdown, model) {
|
|
527
538
|
const policies = requiredPolicies(scope, byId);
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
539
|
+
const documents = modelSupports(model, "governed-document-activation")
|
|
540
|
+
? requiredGovernedDocuments(scope, records, byId, model)
|
|
541
|
+
: [];
|
|
542
|
+
const trainings = modelSupports(model, "governed-training-activation")
|
|
543
|
+
? records.filter(({ type, status }) => type === "training" && !["superseded", "retired"].includes(status))
|
|
544
|
+
: [];
|
|
545
|
+
const governedRecords = [...policies, ...documents, ...trainings];
|
|
546
|
+
const appointedReviewer = governedRecords
|
|
547
|
+
.filter((record) => partiesIndependent(record.ownerIds, record.approverIds, byId))
|
|
548
|
+
.flatMap((record) => [...currentPartyPeople(record.approverIds || [], byId)])
|
|
531
549
|
.map((id) => byId.get(id))
|
|
532
550
|
.find(Boolean);
|
|
533
|
-
const policyOwnerIds = new Set(
|
|
534
|
-
[...partyPeople(
|
|
551
|
+
const policyOwnerIds = new Set(governedRecords.flatMap((record) => (
|
|
552
|
+
[...partyPeople(record.ownerIds || [], byId)]
|
|
535
553
|
)));
|
|
536
554
|
const oversight = byId.get("team-security-risk-oversight");
|
|
537
555
|
const availableReviewer = oversight?.type === "team" && oversight.status === "active"
|
|
@@ -540,7 +558,7 @@ async function policiesStage(scope, records, byId, readMarkdown) {
|
|
|
540
558
|
.map((id) => byId.get(id))
|
|
541
559
|
.find(Boolean)
|
|
542
560
|
: null;
|
|
543
|
-
const reviewerNeedsAssignment = !appointedReviewer && availableReviewer &&
|
|
561
|
+
const reviewerNeedsAssignment = !appointedReviewer && availableReviewer && governedRecords.length;
|
|
544
562
|
const items = [
|
|
545
563
|
item(
|
|
546
564
|
"independent-reviewer",
|
|
@@ -553,7 +571,7 @@ async function policiesStage(scope, records, byId, readMarkdown) {
|
|
|
553
571
|
: reviewerNeedsAssignment
|
|
554
572
|
? `${availableReviewer.title} chairs Security and Risk Oversight. Assign this person as approver on each policy after review.`
|
|
555
573
|
: "Appoint a reviewer who is separate from the policy owner. The reviewer may be another person in the organization or an external person, and is separate from the CPA firm that may later perform the audit.",
|
|
556
|
-
appointedReviewer || (reviewerNeedsAssignment ?
|
|
574
|
+
appointedReviewer || (reviewerNeedsAssignment ? governedRecords[0] : { type: "person" }),
|
|
557
575
|
{
|
|
558
576
|
commands: [
|
|
559
577
|
"npx filegrc list appointment --workflow --json",
|
|
@@ -610,10 +628,97 @@ async function policiesStage(scope, records, byId, readMarkdown) {
|
|
|
610
628
|
}
|
|
611
629
|
));
|
|
612
630
|
}
|
|
631
|
+
for (const document of documents) {
|
|
632
|
+
const source = await readMarkdown(document);
|
|
633
|
+
const placeholderCount = openPlaceholderCount(source);
|
|
634
|
+
const isSecurityIncidentRecoveryPlan = document.id === "document-security-incident-recovery-plan";
|
|
635
|
+
const systemsWithCompleteContinuityObjectives = scope.systems.filter(({ continuityObjectives }) => (
|
|
636
|
+
Number.isInteger(continuityObjectives?.recoveryTimeHours)
|
|
637
|
+
&& Number.isInteger(continuityObjectives?.recoveryPointHours)
|
|
638
|
+
&& Number.isInteger(continuityObjectives?.maximumTolerableDowntimeHours)
|
|
639
|
+
));
|
|
640
|
+
const checks = {
|
|
641
|
+
independentlyApproved: ["approved", "active"].includes(document.status)
|
|
642
|
+
&& Boolean(document.approvedOn)
|
|
643
|
+
&& Boolean(document.approvedContentRevisions)
|
|
644
|
+
&& partiesIndependent(document.ownerIds, document.approverIds, byId),
|
|
645
|
+
owner: currentPartyPeople(document.ownerIds, byId).size > 0,
|
|
646
|
+
linkedControls: (document.controlIds || []).some((id) => scope.controls.some((control) => control.id === id)),
|
|
647
|
+
contentComplete: substantiveMarkdown(source) && placeholderCount === 0,
|
|
648
|
+
...(isSecurityIncidentRecoveryPlan
|
|
649
|
+
? { systemContinuityObjectives: systemsWithCompleteContinuityObjectives.length === scope.systems.length && scope.systems.length > 0 }
|
|
650
|
+
: {})
|
|
651
|
+
};
|
|
652
|
+
const missing = Object.entries(checks)
|
|
653
|
+
.filter(([, value]) => !value)
|
|
654
|
+
.map(([name]) => governedApprovalCheckLabel(name));
|
|
655
|
+
items.push(item(
|
|
656
|
+
`document-approval-${document.id}`,
|
|
657
|
+
missing.length ? "action" : "complete",
|
|
658
|
+
document.title,
|
|
659
|
+
missing.length
|
|
660
|
+
? `Remaining Step 2 approval work: ${missing.join(", ")}${placeholderCount ? ` (${placeholderCount} open placeholders)` : ""}. Complete and approve the intended values before implementation.`
|
|
661
|
+
: `Independently approved on ${document.approvedOn} and bound to the exact intended values and Markdown revision. Activation remains in Step 3.`,
|
|
662
|
+
document,
|
|
663
|
+
{
|
|
664
|
+
checks,
|
|
665
|
+
placeholderCount,
|
|
666
|
+
...(isSecurityIncidentRecoveryPlan
|
|
667
|
+
? {
|
|
668
|
+
continuityObjectiveSystemIds: systemsWithCompleteContinuityObjectives.map(({ id }) => id),
|
|
669
|
+
missingContinuityObjectiveSystemIds: scope.systems
|
|
670
|
+
.filter(({ id }) => !systemsWithCompleteContinuityObjectives.some((system) => system.id === id))
|
|
671
|
+
.map(({ id }) => id)
|
|
672
|
+
}
|
|
673
|
+
: {}),
|
|
674
|
+
commands: [
|
|
675
|
+
`npx filegrc get ${shellArgument(document.id)} --mutation`,
|
|
676
|
+
`npx filegrc update document ${shellArgument(document.id)} MUTATION.json --json`,
|
|
677
|
+
"npx filegrc program-readiness --json"
|
|
678
|
+
]
|
|
679
|
+
}
|
|
680
|
+
));
|
|
681
|
+
}
|
|
682
|
+
if (modelSupports(model, "governed-training-activation")) {
|
|
683
|
+
for (const training of trainings) {
|
|
684
|
+
const source = await readMarkdown(training);
|
|
685
|
+
const placeholderCount = openPlaceholderCount(source);
|
|
686
|
+
const checks = {
|
|
687
|
+
independentlyApproved: ["approved", "active"].includes(training.status)
|
|
688
|
+
&& Boolean(training.approvedOn)
|
|
689
|
+
&& Boolean(training.approvedContentRevisions)
|
|
690
|
+
&& partiesIndependent(training.ownerIds, training.approverIds, byId),
|
|
691
|
+
owner: currentPartyPeople(training.ownerIds, byId).size > 0,
|
|
692
|
+
linkedControls: (training.controlIds || []).some((id) => scope.controls.some((control) => control.id === id)),
|
|
693
|
+
contentComplete: substantiveMarkdown(source) && placeholderCount === 0
|
|
694
|
+
};
|
|
695
|
+
const missing = Object.entries(checks)
|
|
696
|
+
.filter(([, value]) => !value)
|
|
697
|
+
.map(([name]) => governedApprovalCheckLabel(name));
|
|
698
|
+
items.push(item(
|
|
699
|
+
`training-approval-${training.id}`,
|
|
700
|
+
missing.length ? "action" : "complete",
|
|
701
|
+
training.title,
|
|
702
|
+
missing.length
|
|
703
|
+
? `Remaining Step 2 approval work: ${missing.join(", ")}${placeholderCount ? ` (${placeholderCount} open placeholders)` : ""}. Review and approve the exact Training content before implementation.`
|
|
704
|
+
: `Independently approved on ${training.approvedOn} and bound to the exact Training revision. Activation remains in Step 3.`,
|
|
705
|
+
training,
|
|
706
|
+
{
|
|
707
|
+
checks,
|
|
708
|
+
placeholderCount,
|
|
709
|
+
commands: [
|
|
710
|
+
`npx filegrc get ${shellArgument(training.id)} --mutation`,
|
|
711
|
+
`npx filegrc update training ${shellArgument(training.id)} MUTATION.json --json`,
|
|
712
|
+
"npx filegrc program-readiness --json"
|
|
713
|
+
]
|
|
714
|
+
}
|
|
715
|
+
));
|
|
716
|
+
}
|
|
717
|
+
}
|
|
613
718
|
return stage(
|
|
614
719
|
"policies",
|
|
615
720
|
"Approve Policies",
|
|
616
|
-
"
|
|
721
|
+
"Approve the exact Policy, program Document, and Training content that defines what the organization intends to require. Approval does not prove implementation or activate the content.",
|
|
617
722
|
items
|
|
618
723
|
);
|
|
619
724
|
}
|
|
@@ -627,8 +732,31 @@ function requiredPolicies(scope, byId) {
|
|
|
627
732
|
));
|
|
628
733
|
}
|
|
629
734
|
|
|
630
|
-
|
|
735
|
+
function requiredGovernedDocuments(scope, records, byId, model) {
|
|
631
736
|
const selectedControlIds = new Set(scope.controls.map(({ id }) => id));
|
|
737
|
+
const linkedDocumentIds = new Set(requiredPolicies(scope, byId).flatMap((policy) => policy.relatedDocumentIds || []));
|
|
738
|
+
const obligationDocumentIds = new Set(records
|
|
739
|
+
.filter((record) => record.type === "obligation")
|
|
740
|
+
.flatMap((record) => [
|
|
741
|
+
...(record.scopeResourceIds || []),
|
|
742
|
+
...(record.templateResourceId ? [record.templateResourceId] : [])
|
|
743
|
+
]));
|
|
744
|
+
return records.filter((record) => (
|
|
745
|
+
record.type === "document"
|
|
746
|
+
&& !documentIsAuditSpecific(record, model)
|
|
747
|
+
&& !["superseded", "retired"].includes(record.status)
|
|
748
|
+
&& (
|
|
749
|
+
linkedDocumentIds.has(record.id)
|
|
750
|
+
|| obligationDocumentIds.has(record.id)
|
|
751
|
+
|| (
|
|
752
|
+
record.programRole === "required"
|
|
753
|
+
&& (record.controlIds || []).some((id) => selectedControlIds.has(id))
|
|
754
|
+
)
|
|
755
|
+
)
|
|
756
|
+
));
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
async function governedContentItems(scope, records, byId, readMarkdown, asOf, model) {
|
|
632
760
|
const enabledObligations = records.filter((record) => (
|
|
633
761
|
record.type === "obligation" && obligationIsEnabled(record)
|
|
634
762
|
));
|
|
@@ -636,31 +764,47 @@ async function governedContentItems(scope, records, byId, readMarkdown, asOf) {
|
|
|
636
764
|
...(record.scopeResourceIds || []),
|
|
637
765
|
...(record.templateResourceId ? [record.templateResourceId] : [])
|
|
638
766
|
]));
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
767
|
+
const documents = requiredGovernedDocuments(scope, records, byId, model);
|
|
768
|
+
const governedRecords = [
|
|
769
|
+
...documents,
|
|
770
|
+
...records.filter((record) => (
|
|
771
|
+
record.type === "training"
|
|
772
|
+
&& !["superseded", "retired"].includes(record.status)
|
|
642
773
|
&& (
|
|
643
774
|
requiredGovernedIds.has(record.id)
|
|
644
|
-
|| (
|
|
645
|
-
record.programRole === "required"
|
|
646
|
-
&& (record.controlIds || []).some((id) => selectedControlIds.has(id))
|
|
647
|
-
)
|
|
775
|
+
|| (record.controlIds || []).some((id) => scope.controls.some((control) => control.id === id))
|
|
648
776
|
)
|
|
649
|
-
)
|
|
650
|
-
|
|
651
|
-
));
|
|
777
|
+
))
|
|
778
|
+
];
|
|
652
779
|
const items = [];
|
|
780
|
+
const documentActivations = [];
|
|
781
|
+
const trainingActivations = [];
|
|
653
782
|
for (const record of governedRecords) {
|
|
654
783
|
const source = await readMarkdown(record);
|
|
655
784
|
const placeholderCount = openPlaceholderCount(source);
|
|
656
|
-
const
|
|
657
|
-
const
|
|
658
|
-
Number.isInteger(continuityObjectives?.recoveryTimeHours)
|
|
659
|
-
&& Number.isInteger(continuityObjectives?.recoveryPointHours)
|
|
660
|
-
&& Number.isInteger(continuityObjectives?.maximumTolerableDowntimeHours)
|
|
661
|
-
));
|
|
785
|
+
const linkedControlIds = (record.controlIds || []).filter((id) => scope.controls.some((control) => control.id === id));
|
|
786
|
+
const missingImplementationControlIds = linkedControlIds.filter((id) => byId.get(id)?.status !== "implemented");
|
|
662
787
|
const checks = record.type === "document"
|
|
663
|
-
? {
|
|
788
|
+
? modelSupports(model, "governed-document-activation") ? {
|
|
789
|
+
approvalBound: ["approved", "active"].includes(record.status)
|
|
790
|
+
&& Boolean(record.approvedOn)
|
|
791
|
+
&& Boolean(record.approvedContentRevisions)
|
|
792
|
+
&& partiesIndependent(record.ownerIds, record.approverIds, byId),
|
|
793
|
+
owner: currentPartyPeople(record.ownerIds, byId).size > 0,
|
|
794
|
+
active: record.status === "active",
|
|
795
|
+
requirementsImplemented: linkedControlIds.length > 0 && missingImplementationControlIds.length === 0,
|
|
796
|
+
activationRecorded: record.activationBasis === "recorded",
|
|
797
|
+
activated: Boolean(record.activatedOn),
|
|
798
|
+
activator: Boolean((record.activatedByIds || []).length)
|
|
799
|
+
&& record.activatedByIds.every((id) => personWasActiveOn(byId.get(id), record.activatedOn)),
|
|
800
|
+
activatedContent: Boolean(record.activatedContentRevisions),
|
|
801
|
+
activationMatchesApproval: contentRevisionBindingsMatch(
|
|
802
|
+
record.approvedContentRevisions,
|
|
803
|
+
record.activatedContentRevisions
|
|
804
|
+
),
|
|
805
|
+
effective: Boolean(record.effectiveOn && record.effectiveOn <= asOf),
|
|
806
|
+
contentComplete: substantiveMarkdown(source) && placeholderCount === 0
|
|
807
|
+
} : {
|
|
664
808
|
active: record.status === "active",
|
|
665
809
|
owner: currentPartyPeople(record.ownerIds, byId).size > 0,
|
|
666
810
|
independentlyApproved: Boolean(
|
|
@@ -668,12 +812,32 @@ async function governedContentItems(scope, records, byId, readMarkdown, asOf) {
|
|
|
668
812
|
&& partiesIndependent(record.ownerIds, record.approverIds, byId)
|
|
669
813
|
),
|
|
670
814
|
effective: Boolean(record.effectiveOn && record.effectiveOn <= asOf),
|
|
671
|
-
contentComplete: substantiveMarkdown(source) && placeholderCount === 0
|
|
672
|
-
...(isSecurityIncidentRecoveryPlan
|
|
673
|
-
? { systemContinuityObjectives: systemsWithCompleteContinuityObjectives.length === scope.systems.length && scope.systems.length > 0 }
|
|
674
|
-
: {})
|
|
815
|
+
contentComplete: substantiveMarkdown(source) && placeholderCount === 0
|
|
675
816
|
}
|
|
676
|
-
: {
|
|
817
|
+
: modelSupports(model, "governed-training-activation") ? {
|
|
818
|
+
approvalBound: ["approved", "active"].includes(record.status)
|
|
819
|
+
&& Boolean(record.approvedOn)
|
|
820
|
+
&& Boolean(record.approvedContentRevisions)
|
|
821
|
+
&& partiesIndependent(record.ownerIds, record.approverIds, byId),
|
|
822
|
+
owner: currentPartyPeople(record.ownerIds, byId).size > 0,
|
|
823
|
+
active: record.status === "active",
|
|
824
|
+
requirementsImplemented: linkedControlIds.length > 0 && missingImplementationControlIds.length === 0,
|
|
825
|
+
assignmentScheduled: enabledObligations.some((obligation) => (
|
|
826
|
+
obligation.templateResourceId === record.id
|
|
827
|
+
|| (obligation.scopeResourceIds || []).includes(record.id)
|
|
828
|
+
)),
|
|
829
|
+
activationRecorded: ["recorded", "legacy-v5"].includes(record.activationBasis),
|
|
830
|
+
activated: record.activationBasis === "legacy-v5" || Boolean(record.activatedOn),
|
|
831
|
+
activator: record.activationBasis === "legacy-v5" || Boolean((record.activatedByIds || []).length)
|
|
832
|
+
&& record.activatedByIds.every((id) => personWasActiveOn(byId.get(id), record.activatedOn)),
|
|
833
|
+
activatedContent: record.activationBasis === "legacy-v5" || Boolean(record.activatedContentRevisions),
|
|
834
|
+
activationMatchesApproval: record.activationBasis === "legacy-v5" || contentRevisionBindingsMatch(
|
|
835
|
+
record.approvedContentRevisions,
|
|
836
|
+
record.activatedContentRevisions
|
|
837
|
+
),
|
|
838
|
+
effective: Boolean(record.effectiveOn && record.effectiveOn <= asOf),
|
|
839
|
+
contentComplete: substantiveMarkdown(source) && placeholderCount === 0
|
|
840
|
+
} : {
|
|
677
841
|
active: record.status === "active",
|
|
678
842
|
owner: currentPartyPeople(record.ownerIds, byId).size > 0,
|
|
679
843
|
approved: Boolean(record.approvedOn && (record.approvedByIds || []).length),
|
|
@@ -684,6 +848,70 @@ async function governedContentItems(scope, records, byId, readMarkdown, asOf) {
|
|
|
684
848
|
const missing = Object.entries(checks)
|
|
685
849
|
.filter(([, value]) => !value)
|
|
686
850
|
.map(([name]) => governedContentCheckLabel(name));
|
|
851
|
+
if (record.type === "document" && modelSupports(model, "governed-document-activation")) {
|
|
852
|
+
const activationComplete = Object.values(checks).every(Boolean);
|
|
853
|
+
const preActivationCheckNames = ["approvalBound", "owner", "requirementsImplemented", "contentComplete"];
|
|
854
|
+
const preActivationGapCount = preActivationCheckNames.filter((name) => !checks[name]).length;
|
|
855
|
+
const readyToActivate = record.status === "approved"
|
|
856
|
+
&& checks.approvalBound
|
|
857
|
+
&& checks.owner
|
|
858
|
+
&& checks.requirementsImplemented
|
|
859
|
+
&& checks.contentComplete;
|
|
860
|
+
const state = record.status === "active" && activationComplete
|
|
861
|
+
? "active-and-operating"
|
|
862
|
+
: record.status === "active"
|
|
863
|
+
? "active-with-gaps"
|
|
864
|
+
: readyToActivate
|
|
865
|
+
? "ready-to-activate"
|
|
866
|
+
: record.status === "approved"
|
|
867
|
+
? "approved-implementation-pending"
|
|
868
|
+
: "approval-pending";
|
|
869
|
+
documentActivations.push({
|
|
870
|
+
documentId: record.id,
|
|
871
|
+
title: record.title,
|
|
872
|
+
state,
|
|
873
|
+
label: documentActivationLabel(state),
|
|
874
|
+
approvedOn: record.approvedOn || null,
|
|
875
|
+
activatedOn: record.activatedOn || null,
|
|
876
|
+
effectiveOn: record.effectiveOn || null,
|
|
877
|
+
linkedControlIds,
|
|
878
|
+
missingImplementationControlIds,
|
|
879
|
+
activationRevisionBound: Boolean(record.activatedContentRevisions),
|
|
880
|
+
activatedByIds: record.activatedByIds || [],
|
|
881
|
+
gapCount: record.status === "active" ? missing.length : preActivationGapCount
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
if (record.type === "training" && modelSupports(model, "governed-training-activation")) {
|
|
885
|
+
const activationComplete = Object.values(checks).every(Boolean);
|
|
886
|
+
const preActivationCheckNames = ["approvalBound", "owner", "requirementsImplemented", "assignmentScheduled", "contentComplete"];
|
|
887
|
+
const preActivationGapCount = preActivationCheckNames.filter((name) => !checks[name]).length;
|
|
888
|
+
const readyToActivate = record.status === "approved"
|
|
889
|
+
&& preActivationCheckNames.every((name) => checks[name]);
|
|
890
|
+
const state = record.status === "active" && activationComplete
|
|
891
|
+
? "active-and-operating"
|
|
892
|
+
: record.status === "active"
|
|
893
|
+
? "active-with-gaps"
|
|
894
|
+
: readyToActivate
|
|
895
|
+
? "ready-to-activate"
|
|
896
|
+
: record.status === "approved"
|
|
897
|
+
? "approved-implementation-pending"
|
|
898
|
+
: "approval-pending";
|
|
899
|
+
trainingActivations.push({
|
|
900
|
+
trainingId: record.id,
|
|
901
|
+
title: record.title,
|
|
902
|
+
state,
|
|
903
|
+
label: documentActivationLabel(state),
|
|
904
|
+
approvedOn: record.approvedOn || null,
|
|
905
|
+
activatedOn: record.activatedOn || null,
|
|
906
|
+
effectiveOn: record.effectiveOn || null,
|
|
907
|
+
linkedControlIds,
|
|
908
|
+
missingImplementationControlIds,
|
|
909
|
+
assignmentScheduled: checks.assignmentScheduled,
|
|
910
|
+
activationRevisionBound: Boolean(record.activatedContentRevisions),
|
|
911
|
+
activatedByIds: record.activatedByIds || [],
|
|
912
|
+
gapCount: record.status === "active" ? missing.length : preActivationGapCount
|
|
913
|
+
});
|
|
914
|
+
}
|
|
687
915
|
items.push(item(
|
|
688
916
|
`${record.type}-${record.id}`,
|
|
689
917
|
missing.length ? "action" : "complete",
|
|
@@ -691,20 +919,15 @@ async function governedContentItems(scope, records, byId, readMarkdown, asOf) {
|
|
|
691
919
|
missing.length
|
|
692
920
|
? `Remaining governed-content work: ${missing.join(", ")}${placeholderCount ? ` (${placeholderCount} open placeholders)` : ""}.`
|
|
693
921
|
: record.type === "document"
|
|
694
|
-
?
|
|
922
|
+
? modelSupports(model, "governed-document-activation")
|
|
923
|
+
? `Approved on ${record.approvedOn}, activated separately on ${record.activatedOn}, effective ${record.effectiveOn}, revision-bound at both events, and ready for operation.`
|
|
924
|
+
: `Active, approved by a separate reviewer, effective ${record.effectiveOn}, and ready for the selected Controls or running schedule.`
|
|
695
925
|
: `Active, approved, effective ${record.effectiveOn}, revision-bound, and ready for the running training schedule.`,
|
|
696
926
|
record,
|
|
697
927
|
{
|
|
698
928
|
checks,
|
|
699
929
|
placeholderCount,
|
|
700
|
-
...(
|
|
701
|
-
? {
|
|
702
|
-
continuityObjectiveSystemIds: systemsWithCompleteContinuityObjectives.map(({ id }) => id),
|
|
703
|
-
missingContinuityObjectiveSystemIds: scope.systems
|
|
704
|
-
.filter(({ id }) => !systemsWithCompleteContinuityObjectives.some((system) => system.id === id))
|
|
705
|
-
.map(({ id }) => id)
|
|
706
|
-
}
|
|
707
|
-
: {}),
|
|
930
|
+
...(["document", "training"].includes(record.type) ? { linkedControlIds, missingImplementationControlIds } : {}),
|
|
708
931
|
commands: [
|
|
709
932
|
`npx filegrc get ${shellArgument(record.id)} --mutation`,
|
|
710
933
|
`npx filegrc update ${record.type} ${shellArgument(record.id)} MUTATION.json --json`,
|
|
@@ -713,12 +936,22 @@ async function governedContentItems(scope, records, byId, readMarkdown, asOf) {
|
|
|
713
936
|
}
|
|
714
937
|
));
|
|
715
938
|
}
|
|
716
|
-
return items;
|
|
939
|
+
return { items, documentActivations, trainingActivations };
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function documentActivationLabel(state) {
|
|
943
|
+
return ({
|
|
944
|
+
"approval-pending": "Approval pending in Step 2",
|
|
945
|
+
"approved-implementation-pending": "Approved, implementation pending",
|
|
946
|
+
"ready-to-activate": "Ready to activate",
|
|
947
|
+
"active-with-gaps": "Active with activation or implementation gaps",
|
|
948
|
+
"active-and-operating": "Active and operating"
|
|
949
|
+
})[state] || state;
|
|
717
950
|
}
|
|
718
951
|
|
|
719
952
|
async function assessPolicyActivations(policies, controls, records, byId, readMarkdown, asOf, model) {
|
|
720
|
-
const sourceType =
|
|
721
|
-
const sourceField =
|
|
953
|
+
const sourceType = modelSupports(model, "component-sources") ? "component" : "system";
|
|
954
|
+
const sourceField = modelSupports(model, "component-sources") ? "evidenceSourceComponentIds" : "evidenceSourceIds";
|
|
722
955
|
const assessments = [];
|
|
723
956
|
for (const policy of policies.filter((record) => ["approved", "active"].includes(record.status))) {
|
|
724
957
|
const linkedControls = controls.filter((control) => (control.policyIds || []).includes(policy.id));
|
|
@@ -726,7 +959,7 @@ async function assessPolicyActivations(policies, controls, records, byId, readMa
|
|
|
726
959
|
const plannedOrPartialControlIds = linkedControls
|
|
727
960
|
.filter((control) => ["planned", "partially-implemented"].includes(control.status))
|
|
728
961
|
.map(({ id }) => id);
|
|
729
|
-
const missingComponentControlIds =
|
|
962
|
+
const missingComponentControlIds = modelSupports(model, "component-sources")
|
|
730
963
|
? linkedControls.filter((control) => ![
|
|
731
964
|
...(control.componentIds || []),
|
|
732
965
|
...(control.evidenceSourceComponentIds || [])
|
|
@@ -759,6 +992,18 @@ async function assessPolicyActivations(policies, controls, records, byId, readMa
|
|
|
759
992
|
&& (record.policyIds || []).includes(policy.id)
|
|
760
993
|
))
|
|
761
994
|
)).map(({ id }) => id);
|
|
995
|
+
const linkedGovernedDocumentIds = modelSupports(model, "governed-document-activation")
|
|
996
|
+
? (policy.relatedDocumentIds || []).filter((id) => {
|
|
997
|
+
const document = byId.get(id);
|
|
998
|
+
return document?.type === "document"
|
|
999
|
+
&& !["superseded", "retired"].includes(document.status)
|
|
1000
|
+
&& !documentIsAuditSpecific(document, model);
|
|
1001
|
+
})
|
|
1002
|
+
: [];
|
|
1003
|
+
const missingGovernedDocumentIds = linkedGovernedDocumentIds.filter((id) => {
|
|
1004
|
+
const document = byId.get(id);
|
|
1005
|
+
return !governedDocumentIsOperating(document, asOf, model);
|
|
1006
|
+
});
|
|
762
1007
|
const relevantExceptions = records.filter((record) => (
|
|
763
1008
|
record.type === "exception"
|
|
764
1009
|
&& (record.scopeResourceIds || []).some((id) => id === policy.id || linkedControlIds.includes(id))
|
|
@@ -788,6 +1033,7 @@ async function assessPolicyActivations(policies, controls, records, byId, readMa
|
|
|
788
1033
|
+ missingComponentControlIds.length
|
|
789
1034
|
+ missingEvidenceSourceControlIds.length
|
|
790
1035
|
+ missingScheduleControlIds.length
|
|
1036
|
+
+ missingGovernedDocumentIds.length
|
|
791
1037
|
+ unresolvedExceptionIds.length
|
|
792
1038
|
+ timingWarnings.length;
|
|
793
1039
|
const activeNow = policy.status === "active" && policy.effectiveOn && policy.effectiveOn <= asOf;
|
|
@@ -811,6 +1057,8 @@ async function assessPolicyActivations(policies, controls, records, byId, readMa
|
|
|
811
1057
|
missingComponentControlIds,
|
|
812
1058
|
missingEvidenceSourceControlIds,
|
|
813
1059
|
missingScheduleControlIds,
|
|
1060
|
+
linkedGovernedDocumentIds,
|
|
1061
|
+
missingGovernedDocumentIds,
|
|
814
1062
|
unresolvedExceptionIds,
|
|
815
1063
|
documentedExceptionIds,
|
|
816
1064
|
timingWarnings,
|
|
@@ -840,11 +1088,12 @@ function policyActivationItem(assessment) {
|
|
|
840
1088
|
[assessment.plannedOrPartialControlIds.length, "planned or partial Controls"],
|
|
841
1089
|
[assessment.missingComponentControlIds.length, "Controls missing active Components"],
|
|
842
1090
|
[assessment.missingEvidenceSourceControlIds.length, "Controls missing ready evidence sources"],
|
|
843
|
-
[assessment.missingScheduleControlIds.length, "Controls missing enabled
|
|
1091
|
+
[assessment.missingScheduleControlIds.length, "Controls missing enabled Obligations"],
|
|
1092
|
+
[assessment.missingGovernedDocumentIds?.length || 0, "required governed Documents not active"],
|
|
844
1093
|
[assessment.unresolvedExceptionIds.length, "unresolved Exceptions"]
|
|
845
1094
|
].filter(([count]) => count).map(([count, label]) => `${count} ${label}`);
|
|
846
1095
|
const message = assessment.state === "active-and-operating"
|
|
847
|
-
? `Active and effective ${assessment.effectiveOn}; all ${assessment.linkedControlIds.length} linked Controls are implemented with Components, evidence sources, and enabled
|
|
1096
|
+
? `Active and effective ${assessment.effectiveOn}; all ${assessment.linkedControlIds.length} linked Controls are implemented with Components, evidence sources, and enabled Obligations.`
|
|
848
1097
|
: assessment.state === "ready-to-activate"
|
|
849
1098
|
? "Implementation checks are complete. Include this approved Policy in the Step 3 cutover when you are ready for it to take effect."
|
|
850
1099
|
: `${assessment.label}: ${counts.join(", ") || assessment.timingWarnings.join(" ")}. ${assessment.activationWarning || ""}`.trim();
|
|
@@ -893,10 +1142,10 @@ async function controlsStage(scope, byId, readMarkdown, asOf, model) {
|
|
|
893
1142
|
for (const control of scope.controls) {
|
|
894
1143
|
const source = await readMarkdown(control);
|
|
895
1144
|
const sourceSystems = (
|
|
896
|
-
|
|
1145
|
+
modelSupports(model, "component-sources")
|
|
897
1146
|
? control.evidenceSourceComponentIds || []
|
|
898
1147
|
: control.evidenceSourceIds || []
|
|
899
|
-
).map((id) => byId.get(id)).filter((record) => record?.type === (
|
|
1148
|
+
).map((id) => byId.get(id)).filter((record) => record?.type === (modelSupports(model, "component-sources") ? "component" : "system"));
|
|
900
1149
|
const queueSchedules = [...byId.values()].filter((record) => (
|
|
901
1150
|
record.type === "obligation"
|
|
902
1151
|
&& record.status !== "retired"
|
|
@@ -951,20 +1200,20 @@ async function controlsStage(scope, byId, readMarkdown, asOf, model) {
|
|
|
951
1200
|
],
|
|
952
1201
|
workQueue: queueSchedules.length ? {
|
|
953
1202
|
enabled: queueSchedules.filter(obligationIsEnabled).length,
|
|
954
|
-
running: queueSchedules.filter((obligation) => obligationIsRunning(obligation, byId, asOf)).length,
|
|
1203
|
+
running: queueSchedules.filter((obligation) => obligationIsRunning(obligation, byId, asOf, model)).length,
|
|
955
1204
|
total: queueSchedules.length
|
|
956
1205
|
} : null
|
|
957
1206
|
}
|
|
958
1207
|
));
|
|
959
1208
|
}
|
|
960
|
-
return stage("controls", "Implement Controls", "Each implemented Control needs an owner, actual procedure, scope, operation pattern, evidence source, mappings, an implementation date, and any required
|
|
1209
|
+
return stage("controls", "Implement Controls", "Each implemented Control needs an owner, actual procedure, scope, operation pattern, evidence source, mappings, an implementation date, and any required Obligations enabled. Scheduled work stays dormant until its governing Policy, program Documents, and Training are active and effective.", items);
|
|
961
1210
|
}
|
|
962
1211
|
|
|
963
1212
|
async function evidenceSourcesStage(scope, byId, model, readMarkdown) {
|
|
964
1213
|
const families = selectedControlFamilies(scope.controls, model);
|
|
965
1214
|
const items = [];
|
|
966
1215
|
for (const family of families) {
|
|
967
|
-
const componentSources =
|
|
1216
|
+
const componentSources = modelSupports(model, "component-sources");
|
|
968
1217
|
const sourceField = componentSources ? "evidenceSourceComponentIds" : "evidenceSourceIds";
|
|
969
1218
|
const sourceType = componentSources ? "component" : "system";
|
|
970
1219
|
const selectedSources = [...new Set(family.controls.flatMap((control) => control[sourceField] || []))]
|
|
@@ -1056,7 +1305,7 @@ async function evidenceSourcesStage(scope, byId, model, readMarkdown) {
|
|
|
1056
1305
|
}
|
|
1057
1306
|
));
|
|
1058
1307
|
}
|
|
1059
|
-
return stage("sources", "Control Evidence Sources", `Complete the authoritative ${
|
|
1308
|
+
return stage("sources", "Control Evidence Sources", `Complete the authoritative ${modelSupports(model, "component-sources") ? "Components" : "Systems"} for every selected control family before marking the Controls implemented.`, items);
|
|
1060
1309
|
}
|
|
1061
1310
|
|
|
1062
1311
|
function operationStage(loaded, workspace, scope, records, byId, asOf, evidenceReady, model) {
|
|
@@ -1271,14 +1520,32 @@ function policyCheckLabel(name) {
|
|
|
1271
1520
|
})[name] || name;
|
|
1272
1521
|
}
|
|
1273
1522
|
|
|
1523
|
+
function governedApprovalCheckLabel(name) {
|
|
1524
|
+
return ({
|
|
1525
|
+
independentlyApproved: "independent approval and approval date",
|
|
1526
|
+
owner: "current owner",
|
|
1527
|
+
linkedControls: "linked Controls",
|
|
1528
|
+
contentComplete: "intended values, content, and organization placeholders",
|
|
1529
|
+
systemContinuityObjectives: "RTO, RPO, and maximum tolerable downtime for every in-scope System"
|
|
1530
|
+
})[name] || name;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1274
1533
|
function governedContentCheckLabel(name) {
|
|
1275
1534
|
return ({
|
|
1535
|
+
approvalBound: "Step 2 independent approval and approved revision",
|
|
1276
1536
|
active: "active status",
|
|
1277
1537
|
owner: "current owner",
|
|
1278
1538
|
independentlyApproved: "independent approval and approval date",
|
|
1279
1539
|
approved: "approval and approval date",
|
|
1280
1540
|
effective: "effective date",
|
|
1281
1541
|
effectiveContent: "effective content revision",
|
|
1542
|
+
requirementsImplemented: "implemented linked requirements",
|
|
1543
|
+
assignmentScheduled: "enabled Training assignment schedule",
|
|
1544
|
+
activationRecorded: "recorded activation basis",
|
|
1545
|
+
activated: "separate activation date",
|
|
1546
|
+
activator: "named activation Person",
|
|
1547
|
+
activatedContent: "separate activated content revision",
|
|
1548
|
+
activationMatchesApproval: "unchanged approved revision at activation",
|
|
1282
1549
|
contentComplete: "content and organization placeholders",
|
|
1283
1550
|
systemContinuityObjectives: "RTO, RPO, and maximum tolerable downtime for every in-scope System"
|
|
1284
1551
|
})[name] || name;
|
|
@@ -1299,7 +1566,7 @@ function controlCheckLabel(name) {
|
|
|
1299
1566
|
implementationReview: "independent implementation review",
|
|
1300
1567
|
policyMapping: "policy mapping",
|
|
1301
1568
|
criteriaMapping: "criteria mapping",
|
|
1302
|
-
workQueue: "running
|
|
1569
|
+
workQueue: "running Obligation schedules"
|
|
1303
1570
|
})[name] || name;
|
|
1304
1571
|
}
|
|
1305
1572
|
|