filegrc 0.7.0 → 0.8.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 +14 -6
- package/model/index.js +21 -4
- package/model/v4.json +97 -6
- package/model/v5.json +10233 -0
- package/package.json +4 -2
- package/src/agent.js +4 -3
- package/src/audit-preparation.js +635 -81
- package/src/audit-transition.js +8 -2
- package/src/batch-review.js +21 -11
- package/src/cli.js +117 -12
- package/src/collection-review.js +4 -3
- package/src/collection-scope.js +4 -3
- package/src/document-activation.js +145 -0
- package/src/evidence-packet.js +393 -106
- package/src/external-reviewer.js +5 -4
- package/src/files.js +194 -35
- package/src/git.js +106 -10
- package/src/index.js +10 -1
- package/src/model-migration.js +218 -7
- package/src/obligations.js +14 -11
- package/src/policy-library/information-security-policy-v2.md +290 -0
- package/src/policy-library.js +827 -0
- package/src/program-lifecycle.js +93 -3
- package/src/program-path.js +14 -9
- package/src/program-readiness.js +306 -75
- package/src/program.js +5 -3
- package/src/reconciliation.js +3 -2
- package/src/server.js +29 -7
- package/src/setup.js +25 -5
- package/src/soc2.js +228 -0
- package/src/state.js +8 -0
- package/src/validate.js +168 -14
- package/src/web.js +257 -35
- package/src/workflow.js +107 -44
- package/src/workspace.js +5 -0
package/src/program-readiness.js
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
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";
|
|
17
|
+
import { assessPolicyLibraryUpgrades } from "./policy-library.js";
|
|
10
18
|
import { programComponents, resolveProgram, selectedRequirementIds } from "./program.js";
|
|
11
19
|
import { markdownEntries } from "./resource-markdown.js";
|
|
20
|
+
import {
|
|
21
|
+
missingSoc2References,
|
|
22
|
+
personWasActiveOn,
|
|
23
|
+
REQUIRED_SOC2_DESCRIPTION_REFERENCES,
|
|
24
|
+
REQUIRED_SOC2_SECURITY_REFERENCES
|
|
25
|
+
} from "./soc2.js";
|
|
12
26
|
import { assessSourceCoverageReadiness } from "./source-coverage.js";
|
|
13
27
|
import { currentCalendarDate } from "./time.js";
|
|
14
28
|
import { loadWorkspace } from "./workspace.js";
|
|
@@ -31,14 +45,15 @@ export async function assessProgramReadiness(input, options = {}) {
|
|
|
31
45
|
return markdown.get(record.id);
|
|
32
46
|
};
|
|
33
47
|
|
|
34
|
-
const policyStage = await policiesStage(scope, records, byId, readMarkdown);
|
|
48
|
+
const policyStage = await policiesStage(scope, records, byId, readMarkdown, loaded.model);
|
|
35
49
|
const controlStage = await controlsStage(scope, byId, readMarkdown, asOf, loaded.model);
|
|
36
50
|
controlStage.items.unshift(...collectionReviews
|
|
37
51
|
.filter(({ resourceType }) => resourceType === "complementary-control")
|
|
38
52
|
.map(collectionReviewReadinessItem));
|
|
39
53
|
const sourceStage = await evidenceSourcesStage(scope, byId, loaded.model, readMarkdown);
|
|
40
54
|
controlStage.items.push(...sourceStage.items);
|
|
41
|
-
|
|
55
|
+
const governedContent = await governedContentItems(scope, records, byId, readMarkdown, asOf, loaded.model);
|
|
56
|
+
controlStage.items.push(...governedContent.items);
|
|
42
57
|
const policyActivations = await assessPolicyActivations(
|
|
43
58
|
requiredPolicies(scope, byId),
|
|
44
59
|
scope.controls,
|
|
@@ -49,7 +64,7 @@ export async function assessProgramReadiness(input, options = {}) {
|
|
|
49
64
|
loaded.model
|
|
50
65
|
);
|
|
51
66
|
controlStage.items.push(...policyActivations.map(policyActivationItem));
|
|
52
|
-
controlStage.description = `Each implemented Control needs an owner, actual procedure, scope, operation pattern, mappings, an implementation date, enabled schedules, and complete authoritative source ${
|
|
67
|
+
controlStage.description = `Each implemented Control needs an owner, actual procedure, scope, operation pattern, mappings, an implementation date, enabled schedules, and complete authoritative source ${modelSupports(loaded.model, "component-sources") ? "Components" : "Systems"}. Activate the unchanged approved governed Documents after their requirements are implemented, then activate approved Policies at the implementation cutover.`;
|
|
53
68
|
const evidenceGateStages = [
|
|
54
69
|
scopeStage(
|
|
55
70
|
program,
|
|
@@ -75,6 +90,7 @@ export async function assessProgramReadiness(input, options = {}) {
|
|
|
75
90
|
&& coverageStart(program.candidateCoverage) <= asOf
|
|
76
91
|
);
|
|
77
92
|
const obligations = planObligations(records, { asOf, through: asOf, model: loaded.model });
|
|
93
|
+
const policyLibrary = await assessPolicyLibraryUpgrades(loaded);
|
|
78
94
|
const operating = evidenceReady && candidateStarted && stages.at(-1).counts.action === 0;
|
|
79
95
|
const canStartCandidatePeriod = Boolean(
|
|
80
96
|
evidenceReady
|
|
@@ -103,7 +119,11 @@ export async function assessProgramReadiness(input, options = {}) {
|
|
|
103
119
|
canStartCandidatePeriod,
|
|
104
120
|
suggestedCandidatePeriodStart: canStartCandidatePeriod ? asOf : null,
|
|
105
121
|
policyActivations,
|
|
106
|
-
|
|
122
|
+
documentActivations: governedContent.documentActivations,
|
|
123
|
+
policyLibraryProposals: [
|
|
124
|
+
...policyLibrary.proposals,
|
|
125
|
+
...legacyPolicyLibraryProposals(records)
|
|
126
|
+
],
|
|
107
127
|
progress: {
|
|
108
128
|
complete,
|
|
109
129
|
total: managedItems.length,
|
|
@@ -135,9 +155,9 @@ export async function assessEvidenceMap(input, options = {}) {
|
|
|
135
155
|
status: counts.action ? "action" : "complete",
|
|
136
156
|
counts,
|
|
137
157
|
workflow: [
|
|
138
|
-
`Choose an existing ${
|
|
139
|
-
`On every source ${
|
|
140
|
-
`Map each selected Control to the authoritative source ${
|
|
158
|
+
`Choose an existing ${modelSupports(readiness.dataModelVersion, "component-sources") ? "Component" : "System"} or create one that is authoritative for each evidence family.`,
|
|
159
|
+
`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.`,
|
|
160
|
+
`Map each selected Control to the authoritative source ${modelSupports(readiness.dataModelVersion, "component-sources") ? "Components with evidenceSourceComponentIds" : "Systems with evidenceSourceIds"} that produce its evidence.`,
|
|
141
161
|
"Run program-readiness again and resolve every incomplete source check and control mapping before marking the Controls implemented."
|
|
142
162
|
],
|
|
143
163
|
items: evidenceItems
|
|
@@ -159,7 +179,7 @@ function programScope(program, records, byId, model, loaded) {
|
|
|
159
179
|
)),
|
|
160
180
|
requirements: select(selectedRequirementIds(program, model), "requirement", (record) => (
|
|
161
181
|
record.type === "requirement" && record.applicability === "applicable"
|
|
162
|
-
)).filter((record) =>
|
|
182
|
+
)).filter((record) => modelSupports(model, "program-scope") || record.applicability === "applicable"),
|
|
163
183
|
controls: select(program?.controlIds, "control", (record) => (
|
|
164
184
|
record.type === "control" && !["not-applicable", "retired"].includes(record.status)
|
|
165
185
|
)).filter((record) => !["not-applicable", "retired"].includes(record.status))
|
|
@@ -185,7 +205,7 @@ function scopeStage(workspace, scope, records, byId, model, collectionReviews =
|
|
|
185
205
|
}
|
|
186
206
|
));
|
|
187
207
|
|
|
188
|
-
if (
|
|
208
|
+
if (modelSupports(model, "program-scope")) {
|
|
189
209
|
const completeComponents = scope.components.filter((component) => (
|
|
190
210
|
component.status === "active"
|
|
191
211
|
&& component.description
|
|
@@ -216,8 +236,8 @@ function scopeStage(workspace, scope, records, byId, model, collectionReviews =
|
|
|
216
236
|
|
|
217
237
|
const completeSystems = scope.systems.filter((system) => (
|
|
218
238
|
system.status === "active"
|
|
219
|
-
&& (
|
|
220
|
-
&& system.classificationId
|
|
239
|
+
&& (modelSupports(model, "program-scope") ? system.purpose && system.boundary && (system.servicesProvided || []).length : system.description)
|
|
240
|
+
&& (modelSupports(model, "program-scope") || system.classificationId)
|
|
221
241
|
&& (system.ownerIds || []).length
|
|
222
242
|
));
|
|
223
243
|
items.push(item(
|
|
@@ -225,12 +245,12 @@ function scopeStage(workspace, scope, records, byId, model, collectionReviews =
|
|
|
225
245
|
scope.systems.length && completeSystems.length === scope.systems.length ? "complete" : "action",
|
|
226
246
|
"Define the service boundary",
|
|
227
247
|
scope.systems.length
|
|
228
|
-
? `${completeSystems.length} of ${scope.systems.length} program systems are active, explicitly in scope, owned,
|
|
248
|
+
? `${completeSystems.length} of ${scope.systems.length} program systems are active, explicitly in scope, owned, and described${modelSupports(model, "program-scope") ? "" : ", with a classification"}.`
|
|
229
249
|
: "Select and describe every service and supporting system in the program boundary.",
|
|
230
250
|
scope.systems[0] || { type: "system" }
|
|
231
251
|
));
|
|
232
252
|
|
|
233
|
-
if (
|
|
253
|
+
if (modelSupports(model, "guided-workflow")) {
|
|
234
254
|
const commitments = records.filter((record) => (
|
|
235
255
|
record.type === "commitment"
|
|
236
256
|
&& !["superseded", "retired"].includes(record.status)
|
|
@@ -240,7 +260,7 @@ function scopeStage(workspace, scope, records, byId, model, collectionReviews =
|
|
|
240
260
|
record.status === "active"
|
|
241
261
|
&& record.statement
|
|
242
262
|
&& record.effectiveOn
|
|
243
|
-
&& (
|
|
263
|
+
&& (modelSupports(model, "program-scope") || record.applicabilityReview?.decision === "applicable")
|
|
244
264
|
&& currentPartyPeople(record.ownerIds, byId).size > 0
|
|
245
265
|
&& (record.requirementIds || []).length > 0
|
|
246
266
|
&& (record.controlIds || []).length > 0
|
|
@@ -273,30 +293,77 @@ function scopeStage(workspace, scope, records, byId, model, collectionReviews =
|
|
|
273
293
|
const applicableRequirements = records.filter((record) => (
|
|
274
294
|
record.type === "requirement"
|
|
275
295
|
&& scope.frameworks.some((framework) => framework.id === record.frameworkId)
|
|
276
|
-
&& (
|
|
296
|
+
&& (modelSupports(model, "program-scope") ? v4Decisions.get(record.id) === "applicable" : record.applicability === "applicable")
|
|
277
297
|
));
|
|
278
298
|
const unresolvedRequirements = records.filter((record) => (
|
|
279
299
|
record.type === "requirement"
|
|
280
300
|
&& scope.frameworks.some((framework) => framework.id === record.frameworkId)
|
|
281
|
-
&& (
|
|
301
|
+
&& (modelSupports(model, "program-scope") ? !v4Decisions.has(record.id) || v4Decisions.get(record.id) === "undetermined" : record.applicability === "undetermined")
|
|
282
302
|
));
|
|
283
303
|
const missingRequirements = applicableRequirements.filter((record) => !selectedRequirementIds.has(record.id));
|
|
304
|
+
const selectedDescriptionRequirements = scope.requirements.filter(isDescriptionRequirement);
|
|
305
|
+
const selectedTrustServicesRequirements = scope.requirements.filter((requirement) => !isDescriptionRequirement(requirement));
|
|
306
|
+
const unresolvedDescriptionRequirements = unresolvedRequirements.filter(isDescriptionRequirement);
|
|
307
|
+
const unresolvedTrustServicesRequirements = unresolvedRequirements.filter((requirement) => !isDescriptionRequirement(requirement));
|
|
308
|
+
const uncoveredRequirements = scope.requirements.filter((requirement) => (
|
|
309
|
+
!isDescriptionRequirement(requirement)
|
|
310
|
+
&& !scope.controls.some((control) => (control.requirementIds || []).includes(requirement.id))
|
|
311
|
+
));
|
|
312
|
+
const enforceSoc2Baseline = modelSupports(model, "program-scope")
|
|
313
|
+
&& ["readiness", "soc-2-type-1", "soc-2-type-2"].includes(goal);
|
|
314
|
+
const selectedFrameworkRequirements = records.filter((record) => (
|
|
315
|
+
record.type === "requirement"
|
|
316
|
+
&& scope.frameworks.some((framework) => framework.id === record.frameworkId)
|
|
317
|
+
));
|
|
318
|
+
const securityRequirements = selectedFrameworkRequirements.filter(isSecurityRequirement);
|
|
319
|
+
const descriptionRequirements = selectedFrameworkRequirements.filter(isDescriptionRequirement);
|
|
320
|
+
const missingRequiredSecurityReferences = enforceSoc2Baseline
|
|
321
|
+
? missingSoc2References(securityRequirements, REQUIRED_SOC2_SECURITY_REFERENCES)
|
|
322
|
+
: [];
|
|
323
|
+
const missingRequiredDescriptionReferences = enforceSoc2Baseline
|
|
324
|
+
? missingSoc2References(descriptionRequirements, REQUIRED_SOC2_DESCRIPTION_REFERENCES)
|
|
325
|
+
: [];
|
|
326
|
+
const mandatoryRequirements = enforceSoc2Baseline
|
|
327
|
+
? [...securityRequirements, ...descriptionRequirements].filter(({ reference }) => (
|
|
328
|
+
REQUIRED_SOC2_SECURITY_REFERENCES.includes(String(reference || "").toUpperCase())
|
|
329
|
+
|| REQUIRED_SOC2_DESCRIPTION_REFERENCES.includes(String(reference || "").toUpperCase())
|
|
330
|
+
))
|
|
331
|
+
: [];
|
|
332
|
+
const invalidMandatoryDecisions = mandatoryRequirements.filter((requirement) => (
|
|
333
|
+
v4Decisions.get(requirement.id) !== "applicable"
|
|
334
|
+
));
|
|
284
335
|
const criteriaComplete = Boolean(
|
|
285
336
|
scope.frameworks.length
|
|
286
337
|
&& scope.requirements.length
|
|
287
338
|
&& scope.controls.length
|
|
288
339
|
&& !unresolvedRequirements.length
|
|
289
340
|
&& !missingRequirements.length
|
|
341
|
+
&& !uncoveredRequirements.length
|
|
342
|
+
&& !missingRequiredSecurityReferences.length
|
|
343
|
+
&& !missingRequiredDescriptionReferences.length
|
|
344
|
+
&& !invalidMandatoryDecisions.length
|
|
290
345
|
);
|
|
291
346
|
items.push(item(
|
|
292
347
|
"criteria",
|
|
293
348
|
criteriaComplete ? "complete" : "action",
|
|
294
|
-
"Confirm criteria
|
|
349
|
+
"Confirm Trust Services criteria, Description Criteria, and Controls",
|
|
295
350
|
criteriaComplete
|
|
296
|
-
? `${
|
|
297
|
-
:
|
|
351
|
+
? `${selectedTrustServicesRequirements.length} applicable Trust Services criteria, ${selectedDescriptionRequirements.length} SOC 2 Description Criteria, and ${scope.controls.length} Controls are in scope. Every applicable Trust Services criterion has at least one selected Control; Description Criteria govern the system description and do not map to Controls.`
|
|
352
|
+
: missingRequiredSecurityReferences.length || missingRequiredDescriptionReferences.length
|
|
353
|
+
? `Use the complete SOC 2 baseline. The selected Frameworks omit ${[
|
|
354
|
+
...missingRequiredSecurityReferences,
|
|
355
|
+
...missingRequiredDescriptionReferences
|
|
356
|
+
].join(", ")}.`
|
|
357
|
+
: invalidMandatoryDecisions.length
|
|
358
|
+
? `Mark all 33 Security Common Criteria and all nine Description Criteria applicable for this SOC 2 Program. ${invalidMandatoryDecisions.length} required ${invalidMandatoryDecisions.length === 1 ? "decision is" : "decisions are"} missing, undetermined, or not applicable.`
|
|
359
|
+
: `Resolve the program criteria and Controls. ${unresolvedTrustServicesRequirements.length} Trust Services applicability decisions and ${unresolvedDescriptionRequirements.length} Description Criteria decisions remain undetermined, ${missingRequirements.length} applicable criteria are not selected, and ${uncoveredRequirements.length} selected applicable Trust Services criteria have no selected Control. Description Criteria govern the system description and do not map to Controls.`,
|
|
298
360
|
workspace || { type: "workspace" },
|
|
299
361
|
{
|
|
362
|
+
unresolvedRequirementIds: unresolvedRequirements.map(({ id }) => id),
|
|
363
|
+
missingRequirementIds: missingRequirements.map(({ id }) => id),
|
|
364
|
+
uncoveredRequirementIds: uncoveredRequirements.map(({ id }) => id),
|
|
365
|
+
invalidMandatoryRequirementIds: invalidMandatoryDecisions.map(({ id }) => id),
|
|
366
|
+
missingRequiredReferences: [...missingRequiredSecurityReferences, ...missingRequiredDescriptionReferences],
|
|
300
367
|
commands: [
|
|
301
368
|
"npx filegrc review-applicability --scaffold --type requirement > decisions.json",
|
|
302
369
|
"npx filegrc review-applicability decisions.json --preview --json",
|
|
@@ -466,15 +533,19 @@ function ownershipResolutionReasons(ownerIds, byId) {
|
|
|
466
533
|
});
|
|
467
534
|
}
|
|
468
535
|
|
|
469
|
-
async function policiesStage(scope, records, byId, readMarkdown) {
|
|
536
|
+
async function policiesStage(scope, records, byId, readMarkdown, model) {
|
|
470
537
|
const policies = requiredPolicies(scope, byId);
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
538
|
+
const documents = modelSupports(model, "governed-document-activation")
|
|
539
|
+
? requiredGovernedDocuments(scope, records, byId, model)
|
|
540
|
+
: [];
|
|
541
|
+
const governedRecords = [...policies, ...documents];
|
|
542
|
+
const appointedReviewer = governedRecords
|
|
543
|
+
.filter((record) => partiesIndependent(record.ownerIds, record.approverIds, byId))
|
|
544
|
+
.flatMap((record) => [...currentPartyPeople(record.approverIds || [], byId)])
|
|
474
545
|
.map((id) => byId.get(id))
|
|
475
546
|
.find(Boolean);
|
|
476
|
-
const policyOwnerIds = new Set(
|
|
477
|
-
[...partyPeople(
|
|
547
|
+
const policyOwnerIds = new Set(governedRecords.flatMap((record) => (
|
|
548
|
+
[...partyPeople(record.ownerIds || [], byId)]
|
|
478
549
|
)));
|
|
479
550
|
const oversight = byId.get("team-security-risk-oversight");
|
|
480
551
|
const availableReviewer = oversight?.type === "team" && oversight.status === "active"
|
|
@@ -483,7 +554,7 @@ async function policiesStage(scope, records, byId, readMarkdown) {
|
|
|
483
554
|
.map((id) => byId.get(id))
|
|
484
555
|
.find(Boolean)
|
|
485
556
|
: null;
|
|
486
|
-
const reviewerNeedsAssignment = !appointedReviewer && availableReviewer &&
|
|
557
|
+
const reviewerNeedsAssignment = !appointedReviewer && availableReviewer && governedRecords.length;
|
|
487
558
|
const items = [
|
|
488
559
|
item(
|
|
489
560
|
"independent-reviewer",
|
|
@@ -496,7 +567,7 @@ async function policiesStage(scope, records, byId, readMarkdown) {
|
|
|
496
567
|
: reviewerNeedsAssignment
|
|
497
568
|
? `${availableReviewer.title} chairs Security and Risk Oversight. Assign this person as approver on each policy after review.`
|
|
498
569
|
: "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.",
|
|
499
|
-
appointedReviewer || (reviewerNeedsAssignment ?
|
|
570
|
+
appointedReviewer || (reviewerNeedsAssignment ? governedRecords[0] : { type: "person" }),
|
|
500
571
|
{
|
|
501
572
|
commands: [
|
|
502
573
|
"npx filegrc list appointment --workflow --json",
|
|
@@ -553,10 +624,61 @@ async function policiesStage(scope, records, byId, readMarkdown) {
|
|
|
553
624
|
}
|
|
554
625
|
));
|
|
555
626
|
}
|
|
627
|
+
for (const document of documents) {
|
|
628
|
+
const source = await readMarkdown(document);
|
|
629
|
+
const placeholderCount = openPlaceholderCount(source);
|
|
630
|
+
const isSecurityIncidentRecoveryPlan = document.id === "document-security-incident-recovery-plan";
|
|
631
|
+
const systemsWithCompleteContinuityObjectives = scope.systems.filter(({ continuityObjectives }) => (
|
|
632
|
+
Number.isInteger(continuityObjectives?.recoveryTimeHours)
|
|
633
|
+
&& Number.isInteger(continuityObjectives?.recoveryPointHours)
|
|
634
|
+
&& Number.isInteger(continuityObjectives?.maximumTolerableDowntimeHours)
|
|
635
|
+
));
|
|
636
|
+
const checks = {
|
|
637
|
+
independentlyApproved: ["approved", "active"].includes(document.status)
|
|
638
|
+
&& Boolean(document.approvedOn)
|
|
639
|
+
&& Boolean(document.approvedContentRevisions)
|
|
640
|
+
&& partiesIndependent(document.ownerIds, document.approverIds, byId),
|
|
641
|
+
owner: currentPartyPeople(document.ownerIds, byId).size > 0,
|
|
642
|
+
linkedControls: (document.controlIds || []).some((id) => scope.controls.some((control) => control.id === id)),
|
|
643
|
+
contentComplete: substantiveMarkdown(source) && placeholderCount === 0,
|
|
644
|
+
...(isSecurityIncidentRecoveryPlan
|
|
645
|
+
? { systemContinuityObjectives: systemsWithCompleteContinuityObjectives.length === scope.systems.length && scope.systems.length > 0 }
|
|
646
|
+
: {})
|
|
647
|
+
};
|
|
648
|
+
const missing = Object.entries(checks)
|
|
649
|
+
.filter(([, value]) => !value)
|
|
650
|
+
.map(([name]) => governedApprovalCheckLabel(name));
|
|
651
|
+
items.push(item(
|
|
652
|
+
`document-approval-${document.id}`,
|
|
653
|
+
missing.length ? "action" : "complete",
|
|
654
|
+
document.title,
|
|
655
|
+
missing.length
|
|
656
|
+
? `Remaining Step 2 approval work: ${missing.join(", ")}${placeholderCount ? ` (${placeholderCount} open placeholders)` : ""}. Complete and approve the intended values before implementation.`
|
|
657
|
+
: `Independently approved on ${document.approvedOn} and bound to the exact intended values and Markdown revision. Activation remains in Step 3.`,
|
|
658
|
+
document,
|
|
659
|
+
{
|
|
660
|
+
checks,
|
|
661
|
+
placeholderCount,
|
|
662
|
+
...(isSecurityIncidentRecoveryPlan
|
|
663
|
+
? {
|
|
664
|
+
continuityObjectiveSystemIds: systemsWithCompleteContinuityObjectives.map(({ id }) => id),
|
|
665
|
+
missingContinuityObjectiveSystemIds: scope.systems
|
|
666
|
+
.filter(({ id }) => !systemsWithCompleteContinuityObjectives.some((system) => system.id === id))
|
|
667
|
+
.map(({ id }) => id)
|
|
668
|
+
}
|
|
669
|
+
: {}),
|
|
670
|
+
commands: [
|
|
671
|
+
`npx filegrc get ${shellArgument(document.id)} --mutation`,
|
|
672
|
+
`npx filegrc update document ${shellArgument(document.id)} MUTATION.json --json`,
|
|
673
|
+
"npx filegrc program-readiness --json"
|
|
674
|
+
]
|
|
675
|
+
}
|
|
676
|
+
));
|
|
677
|
+
}
|
|
556
678
|
return stage(
|
|
557
679
|
"policies",
|
|
558
|
-
"Approve Policies",
|
|
559
|
-
"
|
|
680
|
+
"Approve Policies and Plans",
|
|
681
|
+
"Approve Policy requirements and the intended values in required governed plans and schedules. Approval binds exact revisions but does not prove implementation or activate the content.",
|
|
560
682
|
items
|
|
561
683
|
);
|
|
562
684
|
}
|
|
@@ -570,8 +692,31 @@ function requiredPolicies(scope, byId) {
|
|
|
570
692
|
));
|
|
571
693
|
}
|
|
572
694
|
|
|
573
|
-
|
|
695
|
+
function requiredGovernedDocuments(scope, records, byId, model) {
|
|
574
696
|
const selectedControlIds = new Set(scope.controls.map(({ id }) => id));
|
|
697
|
+
const linkedDocumentIds = new Set(requiredPolicies(scope, byId).flatMap((policy) => policy.relatedDocumentIds || []));
|
|
698
|
+
const obligationDocumentIds = new Set(records
|
|
699
|
+
.filter((record) => record.type === "obligation")
|
|
700
|
+
.flatMap((record) => [
|
|
701
|
+
...(record.scopeResourceIds || []),
|
|
702
|
+
...(record.templateResourceId ? [record.templateResourceId] : [])
|
|
703
|
+
]));
|
|
704
|
+
return records.filter((record) => (
|
|
705
|
+
record.type === "document"
|
|
706
|
+
&& !documentIsAuditSpecific(record, model)
|
|
707
|
+
&& !["superseded", "retired"].includes(record.status)
|
|
708
|
+
&& (
|
|
709
|
+
linkedDocumentIds.has(record.id)
|
|
710
|
+
|| obligationDocumentIds.has(record.id)
|
|
711
|
+
|| (
|
|
712
|
+
record.programRole === "required"
|
|
713
|
+
&& (record.controlIds || []).some((id) => selectedControlIds.has(id))
|
|
714
|
+
)
|
|
715
|
+
)
|
|
716
|
+
));
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
async function governedContentItems(scope, records, byId, readMarkdown, asOf, model) {
|
|
575
720
|
const enabledObligations = records.filter((record) => (
|
|
576
721
|
record.type === "obligation" && obligationIsEnabled(record)
|
|
577
722
|
));
|
|
@@ -579,31 +724,39 @@ async function governedContentItems(scope, records, byId, readMarkdown, asOf) {
|
|
|
579
724
|
...(record.scopeResourceIds || []),
|
|
580
725
|
...(record.templateResourceId ? [record.templateResourceId] : [])
|
|
581
726
|
]));
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|| (
|
|
588
|
-
record.programRole === "required"
|
|
589
|
-
&& (record.controlIds || []).some((id) => selectedControlIds.has(id))
|
|
590
|
-
)
|
|
591
|
-
)
|
|
592
|
-
)
|
|
593
|
-
|| (record.type === "training" && requiredGovernedIds.has(record.id))
|
|
594
|
-
));
|
|
727
|
+
const documents = requiredGovernedDocuments(scope, records, byId, model);
|
|
728
|
+
const governedRecords = [
|
|
729
|
+
...documents,
|
|
730
|
+
...records.filter((record) => record.type === "training" && requiredGovernedIds.has(record.id))
|
|
731
|
+
];
|
|
595
732
|
const items = [];
|
|
733
|
+
const documentActivations = [];
|
|
596
734
|
for (const record of governedRecords) {
|
|
597
735
|
const source = await readMarkdown(record);
|
|
598
736
|
const placeholderCount = openPlaceholderCount(source);
|
|
599
|
-
const
|
|
600
|
-
const
|
|
601
|
-
Number.isInteger(continuityObjectives?.recoveryTimeHours)
|
|
602
|
-
&& Number.isInteger(continuityObjectives?.recoveryPointHours)
|
|
603
|
-
&& Number.isInteger(continuityObjectives?.maximumTolerableDowntimeHours)
|
|
604
|
-
));
|
|
737
|
+
const linkedControlIds = (record.controlIds || []).filter((id) => scope.controls.some((control) => control.id === id));
|
|
738
|
+
const missingImplementationControlIds = linkedControlIds.filter((id) => byId.get(id)?.status !== "implemented");
|
|
605
739
|
const checks = record.type === "document"
|
|
606
|
-
? {
|
|
740
|
+
? modelSupports(model, "governed-document-activation") ? {
|
|
741
|
+
approvalBound: ["approved", "active"].includes(record.status)
|
|
742
|
+
&& Boolean(record.approvedOn)
|
|
743
|
+
&& Boolean(record.approvedContentRevisions)
|
|
744
|
+
&& partiesIndependent(record.ownerIds, record.approverIds, byId),
|
|
745
|
+
owner: currentPartyPeople(record.ownerIds, byId).size > 0,
|
|
746
|
+
active: record.status === "active",
|
|
747
|
+
requirementsImplemented: linkedControlIds.length > 0 && missingImplementationControlIds.length === 0,
|
|
748
|
+
activationRecorded: record.activationBasis === "recorded",
|
|
749
|
+
activated: Boolean(record.activatedOn),
|
|
750
|
+
activator: Boolean((record.activatedByIds || []).length)
|
|
751
|
+
&& record.activatedByIds.every((id) => personWasActiveOn(byId.get(id), record.activatedOn)),
|
|
752
|
+
activatedContent: Boolean(record.activatedContentRevisions),
|
|
753
|
+
activationMatchesApproval: contentRevisionBindingsMatch(
|
|
754
|
+
record.approvedContentRevisions,
|
|
755
|
+
record.activatedContentRevisions
|
|
756
|
+
),
|
|
757
|
+
effective: Boolean(record.effectiveOn && record.effectiveOn <= asOf),
|
|
758
|
+
contentComplete: substantiveMarkdown(source) && placeholderCount === 0
|
|
759
|
+
} : {
|
|
607
760
|
active: record.status === "active",
|
|
608
761
|
owner: currentPartyPeople(record.ownerIds, byId).size > 0,
|
|
609
762
|
independentlyApproved: Boolean(
|
|
@@ -611,10 +764,7 @@ async function governedContentItems(scope, records, byId, readMarkdown, asOf) {
|
|
|
611
764
|
&& partiesIndependent(record.ownerIds, record.approverIds, byId)
|
|
612
765
|
),
|
|
613
766
|
effective: Boolean(record.effectiveOn && record.effectiveOn <= asOf),
|
|
614
|
-
contentComplete: substantiveMarkdown(source) && placeholderCount === 0
|
|
615
|
-
...(isSecurityIncidentRecoveryPlan
|
|
616
|
-
? { systemContinuityObjectives: systemsWithCompleteContinuityObjectives.length === scope.systems.length && scope.systems.length > 0 }
|
|
617
|
-
: {})
|
|
767
|
+
contentComplete: substantiveMarkdown(source) && placeholderCount === 0
|
|
618
768
|
}
|
|
619
769
|
: {
|
|
620
770
|
active: record.status === "active",
|
|
@@ -627,6 +777,39 @@ async function governedContentItems(scope, records, byId, readMarkdown, asOf) {
|
|
|
627
777
|
const missing = Object.entries(checks)
|
|
628
778
|
.filter(([, value]) => !value)
|
|
629
779
|
.map(([name]) => governedContentCheckLabel(name));
|
|
780
|
+
if (record.type === "document" && modelSupports(model, "governed-document-activation")) {
|
|
781
|
+
const activationComplete = Object.values(checks).every(Boolean);
|
|
782
|
+
const preActivationCheckNames = ["approvalBound", "owner", "requirementsImplemented", "contentComplete"];
|
|
783
|
+
const preActivationGapCount = preActivationCheckNames.filter((name) => !checks[name]).length;
|
|
784
|
+
const readyToActivate = record.status === "approved"
|
|
785
|
+
&& checks.approvalBound
|
|
786
|
+
&& checks.owner
|
|
787
|
+
&& checks.requirementsImplemented
|
|
788
|
+
&& checks.contentComplete;
|
|
789
|
+
const state = record.status === "active" && activationComplete
|
|
790
|
+
? "active-and-operating"
|
|
791
|
+
: record.status === "active"
|
|
792
|
+
? "active-with-gaps"
|
|
793
|
+
: readyToActivate
|
|
794
|
+
? "ready-to-activate"
|
|
795
|
+
: record.status === "approved"
|
|
796
|
+
? "approved-implementation-pending"
|
|
797
|
+
: "approval-pending";
|
|
798
|
+
documentActivations.push({
|
|
799
|
+
documentId: record.id,
|
|
800
|
+
title: record.title,
|
|
801
|
+
state,
|
|
802
|
+
label: documentActivationLabel(state),
|
|
803
|
+
approvedOn: record.approvedOn || null,
|
|
804
|
+
activatedOn: record.activatedOn || null,
|
|
805
|
+
effectiveOn: record.effectiveOn || null,
|
|
806
|
+
linkedControlIds,
|
|
807
|
+
missingImplementationControlIds,
|
|
808
|
+
activationRevisionBound: Boolean(record.activatedContentRevisions),
|
|
809
|
+
activatedByIds: record.activatedByIds || [],
|
|
810
|
+
gapCount: record.status === "active" ? missing.length : preActivationGapCount
|
|
811
|
+
});
|
|
812
|
+
}
|
|
630
813
|
items.push(item(
|
|
631
814
|
`${record.type}-${record.id}`,
|
|
632
815
|
missing.length ? "action" : "complete",
|
|
@@ -634,20 +817,15 @@ async function governedContentItems(scope, records, byId, readMarkdown, asOf) {
|
|
|
634
817
|
missing.length
|
|
635
818
|
? `Remaining governed-content work: ${missing.join(", ")}${placeholderCount ? ` (${placeholderCount} open placeholders)` : ""}.`
|
|
636
819
|
: record.type === "document"
|
|
637
|
-
?
|
|
820
|
+
? modelSupports(model, "governed-document-activation")
|
|
821
|
+
? `Approved on ${record.approvedOn}, activated separately on ${record.activatedOn}, effective ${record.effectiveOn}, revision-bound at both events, and ready for operation.`
|
|
822
|
+
: `Active, approved by a separate reviewer, effective ${record.effectiveOn}, and ready for the selected Controls or running schedule.`
|
|
638
823
|
: `Active, approved, effective ${record.effectiveOn}, revision-bound, and ready for the running training schedule.`,
|
|
639
824
|
record,
|
|
640
825
|
{
|
|
641
826
|
checks,
|
|
642
827
|
placeholderCount,
|
|
643
|
-
...(
|
|
644
|
-
? {
|
|
645
|
-
continuityObjectiveSystemIds: systemsWithCompleteContinuityObjectives.map(({ id }) => id),
|
|
646
|
-
missingContinuityObjectiveSystemIds: scope.systems
|
|
647
|
-
.filter(({ id }) => !systemsWithCompleteContinuityObjectives.some((system) => system.id === id))
|
|
648
|
-
.map(({ id }) => id)
|
|
649
|
-
}
|
|
650
|
-
: {}),
|
|
828
|
+
...(record.type === "document" ? { linkedControlIds, missingImplementationControlIds } : {}),
|
|
651
829
|
commands: [
|
|
652
830
|
`npx filegrc get ${shellArgument(record.id)} --mutation`,
|
|
653
831
|
`npx filegrc update ${record.type} ${shellArgument(record.id)} MUTATION.json --json`,
|
|
@@ -656,12 +834,22 @@ async function governedContentItems(scope, records, byId, readMarkdown, asOf) {
|
|
|
656
834
|
}
|
|
657
835
|
));
|
|
658
836
|
}
|
|
659
|
-
return items;
|
|
837
|
+
return { items, documentActivations };
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function documentActivationLabel(state) {
|
|
841
|
+
return ({
|
|
842
|
+
"approval-pending": "Approval pending in Step 2",
|
|
843
|
+
"approved-implementation-pending": "Approved, implementation pending",
|
|
844
|
+
"ready-to-activate": "Ready to activate",
|
|
845
|
+
"active-with-gaps": "Active with activation or implementation gaps",
|
|
846
|
+
"active-and-operating": "Active and operating"
|
|
847
|
+
})[state] || state;
|
|
660
848
|
}
|
|
661
849
|
|
|
662
850
|
async function assessPolicyActivations(policies, controls, records, byId, readMarkdown, asOf, model) {
|
|
663
|
-
const sourceType =
|
|
664
|
-
const sourceField =
|
|
851
|
+
const sourceType = modelSupports(model, "component-sources") ? "component" : "system";
|
|
852
|
+
const sourceField = modelSupports(model, "component-sources") ? "evidenceSourceComponentIds" : "evidenceSourceIds";
|
|
665
853
|
const assessments = [];
|
|
666
854
|
for (const policy of policies.filter((record) => ["approved", "active"].includes(record.status))) {
|
|
667
855
|
const linkedControls = controls.filter((control) => (control.policyIds || []).includes(policy.id));
|
|
@@ -669,7 +857,7 @@ async function assessPolicyActivations(policies, controls, records, byId, readMa
|
|
|
669
857
|
const plannedOrPartialControlIds = linkedControls
|
|
670
858
|
.filter((control) => ["planned", "partially-implemented"].includes(control.status))
|
|
671
859
|
.map(({ id }) => id);
|
|
672
|
-
const missingComponentControlIds =
|
|
860
|
+
const missingComponentControlIds = modelSupports(model, "component-sources")
|
|
673
861
|
? linkedControls.filter((control) => ![
|
|
674
862
|
...(control.componentIds || []),
|
|
675
863
|
...(control.evidenceSourceComponentIds || [])
|
|
@@ -702,6 +890,18 @@ async function assessPolicyActivations(policies, controls, records, byId, readMa
|
|
|
702
890
|
&& (record.policyIds || []).includes(policy.id)
|
|
703
891
|
))
|
|
704
892
|
)).map(({ id }) => id);
|
|
893
|
+
const linkedGovernedDocumentIds = modelSupports(model, "governed-document-activation")
|
|
894
|
+
? (policy.relatedDocumentIds || []).filter((id) => {
|
|
895
|
+
const document = byId.get(id);
|
|
896
|
+
return document?.type === "document"
|
|
897
|
+
&& !["superseded", "retired"].includes(document.status)
|
|
898
|
+
&& !documentIsAuditSpecific(document, model);
|
|
899
|
+
})
|
|
900
|
+
: [];
|
|
901
|
+
const missingGovernedDocumentIds = linkedGovernedDocumentIds.filter((id) => {
|
|
902
|
+
const document = byId.get(id);
|
|
903
|
+
return !governedDocumentIsOperating(document, asOf, model);
|
|
904
|
+
});
|
|
705
905
|
const relevantExceptions = records.filter((record) => (
|
|
706
906
|
record.type === "exception"
|
|
707
907
|
&& (record.scopeResourceIds || []).some((id) => id === policy.id || linkedControlIds.includes(id))
|
|
@@ -731,6 +931,7 @@ async function assessPolicyActivations(policies, controls, records, byId, readMa
|
|
|
731
931
|
+ missingComponentControlIds.length
|
|
732
932
|
+ missingEvidenceSourceControlIds.length
|
|
733
933
|
+ missingScheduleControlIds.length
|
|
934
|
+
+ missingGovernedDocumentIds.length
|
|
734
935
|
+ unresolvedExceptionIds.length
|
|
735
936
|
+ timingWarnings.length;
|
|
736
937
|
const activeNow = policy.status === "active" && policy.effectiveOn && policy.effectiveOn <= asOf;
|
|
@@ -754,6 +955,8 @@ async function assessPolicyActivations(policies, controls, records, byId, readMa
|
|
|
754
955
|
missingComponentControlIds,
|
|
755
956
|
missingEvidenceSourceControlIds,
|
|
756
957
|
missingScheduleControlIds,
|
|
958
|
+
linkedGovernedDocumentIds,
|
|
959
|
+
missingGovernedDocumentIds,
|
|
757
960
|
unresolvedExceptionIds,
|
|
758
961
|
documentedExceptionIds,
|
|
759
962
|
timingWarnings,
|
|
@@ -784,6 +987,7 @@ function policyActivationItem(assessment) {
|
|
|
784
987
|
[assessment.missingComponentControlIds.length, "Controls missing active Components"],
|
|
785
988
|
[assessment.missingEvidenceSourceControlIds.length, "Controls missing ready evidence sources"],
|
|
786
989
|
[assessment.missingScheduleControlIds.length, "Controls missing enabled schedules"],
|
|
990
|
+
[assessment.missingGovernedDocumentIds?.length || 0, "required governed Documents not active"],
|
|
787
991
|
[assessment.unresolvedExceptionIds.length, "unresolved Exceptions"]
|
|
788
992
|
].filter(([count]) => count).map(([count, label]) => `${count} ${label}`);
|
|
789
993
|
const message = assessment.state === "active-and-operating"
|
|
@@ -808,7 +1012,7 @@ function policyActivationItem(assessment) {
|
|
|
808
1012
|
);
|
|
809
1013
|
}
|
|
810
1014
|
|
|
811
|
-
function
|
|
1015
|
+
function legacyPolicyLibraryProposals(records) {
|
|
812
1016
|
const legacyIds = [
|
|
813
1017
|
"policy-anti-bribery-corruption",
|
|
814
1018
|
"policy-clear-desk-screen",
|
|
@@ -836,10 +1040,10 @@ async function controlsStage(scope, byId, readMarkdown, asOf, model) {
|
|
|
836
1040
|
for (const control of scope.controls) {
|
|
837
1041
|
const source = await readMarkdown(control);
|
|
838
1042
|
const sourceSystems = (
|
|
839
|
-
|
|
1043
|
+
modelSupports(model, "component-sources")
|
|
840
1044
|
? control.evidenceSourceComponentIds || []
|
|
841
1045
|
: control.evidenceSourceIds || []
|
|
842
|
-
).map((id) => byId.get(id)).filter((record) => record?.type === (
|
|
1046
|
+
).map((id) => byId.get(id)).filter((record) => record?.type === (modelSupports(model, "component-sources") ? "component" : "system"));
|
|
843
1047
|
const queueSchedules = [...byId.values()].filter((record) => (
|
|
844
1048
|
record.type === "obligation"
|
|
845
1049
|
&& record.status !== "retired"
|
|
@@ -894,20 +1098,20 @@ async function controlsStage(scope, byId, readMarkdown, asOf, model) {
|
|
|
894
1098
|
],
|
|
895
1099
|
workQueue: queueSchedules.length ? {
|
|
896
1100
|
enabled: queueSchedules.filter(obligationIsEnabled).length,
|
|
897
|
-
running: queueSchedules.filter((obligation) => obligationIsRunning(obligation, byId, asOf)).length,
|
|
1101
|
+
running: queueSchedules.filter((obligation) => obligationIsRunning(obligation, byId, asOf, model)).length,
|
|
898
1102
|
total: queueSchedules.length
|
|
899
1103
|
} : null
|
|
900
1104
|
}
|
|
901
1105
|
));
|
|
902
1106
|
}
|
|
903
|
-
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 Work Queue schedules enabled. An enabled schedule stays dormant until its governing Policy
|
|
1107
|
+
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 Work Queue schedules enabled. An enabled schedule stays dormant until its governing Policy and required governed Documents are active and effective.", items);
|
|
904
1108
|
}
|
|
905
1109
|
|
|
906
1110
|
async function evidenceSourcesStage(scope, byId, model, readMarkdown) {
|
|
907
1111
|
const families = selectedControlFamilies(scope.controls, model);
|
|
908
1112
|
const items = [];
|
|
909
1113
|
for (const family of families) {
|
|
910
|
-
const componentSources =
|
|
1114
|
+
const componentSources = modelSupports(model, "component-sources");
|
|
911
1115
|
const sourceField = componentSources ? "evidenceSourceComponentIds" : "evidenceSourceIds";
|
|
912
1116
|
const sourceType = componentSources ? "component" : "system";
|
|
913
1117
|
const selectedSources = [...new Set(family.controls.flatMap((control) => control[sourceField] || []))]
|
|
@@ -999,7 +1203,7 @@ async function evidenceSourcesStage(scope, byId, model, readMarkdown) {
|
|
|
999
1203
|
}
|
|
1000
1204
|
));
|
|
1001
1205
|
}
|
|
1002
|
-
return stage("sources", "Control Evidence Sources", `Complete the authoritative ${
|
|
1206
|
+
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);
|
|
1003
1207
|
}
|
|
1004
1208
|
|
|
1005
1209
|
function operationStage(loaded, workspace, scope, records, byId, asOf, evidenceReady, model) {
|
|
@@ -1214,14 +1418,31 @@ function policyCheckLabel(name) {
|
|
|
1214
1418
|
})[name] || name;
|
|
1215
1419
|
}
|
|
1216
1420
|
|
|
1421
|
+
function governedApprovalCheckLabel(name) {
|
|
1422
|
+
return ({
|
|
1423
|
+
independentlyApproved: "independent approval and approval date",
|
|
1424
|
+
owner: "current owner",
|
|
1425
|
+
linkedControls: "linked Controls",
|
|
1426
|
+
contentComplete: "intended values, content, and organization placeholders",
|
|
1427
|
+
systemContinuityObjectives: "RTO, RPO, and maximum tolerable downtime for every in-scope System"
|
|
1428
|
+
})[name] || name;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1217
1431
|
function governedContentCheckLabel(name) {
|
|
1218
1432
|
return ({
|
|
1433
|
+
approvalBound: "Step 2 independent approval and approved revision",
|
|
1219
1434
|
active: "active status",
|
|
1220
1435
|
owner: "current owner",
|
|
1221
1436
|
independentlyApproved: "independent approval and approval date",
|
|
1222
1437
|
approved: "approval and approval date",
|
|
1223
1438
|
effective: "effective date",
|
|
1224
1439
|
effectiveContent: "effective content revision",
|
|
1440
|
+
requirementsImplemented: "implemented linked requirements",
|
|
1441
|
+
activationRecorded: "recorded activation basis",
|
|
1442
|
+
activated: "separate activation date",
|
|
1443
|
+
activator: "named activation Person",
|
|
1444
|
+
activatedContent: "separate activated content revision",
|
|
1445
|
+
activationMatchesApproval: "unchanged approved revision at activation",
|
|
1225
1446
|
contentComplete: "content and organization placeholders",
|
|
1226
1447
|
systemContinuityObjectives: "RTO, RPO, and maximum tolerable downtime for every in-scope System"
|
|
1227
1448
|
})[name] || name;
|
|
@@ -1253,6 +1474,16 @@ function assuranceGoalLabel(goal) {
|
|
|
1253
1474
|
return "No assurance goal selected";
|
|
1254
1475
|
}
|
|
1255
1476
|
|
|
1477
|
+
function isDescriptionRequirement(requirement) {
|
|
1478
|
+
return (requirement?.tags || []).includes("description-criteria")
|
|
1479
|
+
|| /^DC\d+/i.test(requirement?.reference || "");
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
function isSecurityRequirement(requirement) {
|
|
1483
|
+
const tags = requirement?.tags || [];
|
|
1484
|
+
return tags.includes("security") || tags.includes("common-criteria") || /^CC\d+(?:\.|$)/i.test(requirement?.reference || "");
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1256
1487
|
function stage(id, title, description, items) {
|
|
1257
1488
|
return { id, title, description, items };
|
|
1258
1489
|
}
|