filegrc 0.16.12 → 0.16.13
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/package.json +1 -1
- package/src/applicability-scope.js +33 -6
- package/src/batch-review.js +16 -11
- package/src/collection-scope.js +20 -10
- package/src/retention.js +46 -4
package/package.json
CHANGED
|
@@ -27,6 +27,13 @@ const unorderedStringArrayFields = new Set([
|
|
|
27
27
|
"tags"
|
|
28
28
|
]);
|
|
29
29
|
|
|
30
|
+
const applicabilityDecisionFields = {
|
|
31
|
+
requirement: ["frameworkId", "reference", "description", "parentRequirementId"],
|
|
32
|
+
control: ["statement", "requirementIds", "code", "activity", "controlType", "operationMode", "operationPattern", "systemIds", "policyIds", "componentIds", "evidenceSourceComponentIds"],
|
|
33
|
+
commitment: ["commitmentKind", "statement", "systemIds", "sourceResourceIds", "requirementIds", "controlIds", "customerFacing", "reportingRouteRequirements"],
|
|
34
|
+
"complementary-control": ["responsibleParty", "statement", "systemIds", "vendorId", "requirementIds", "commitmentIds", "relatedControlIds", "sourceDocumentIds", "componentIds"]
|
|
35
|
+
};
|
|
36
|
+
|
|
30
37
|
export function applicabilityScopeRevision(record, program, resources, model, options = {}) {
|
|
31
38
|
const scopedResource = ["commitment", "control", "complementary-control"].includes(record?.type)
|
|
32
39
|
&& !options.legacyBroadScope && !options.legacyBroadCommitment;
|
|
@@ -83,8 +90,7 @@ export function applicabilityScopeRevision(record, program, resources, model, op
|
|
|
83
90
|
.sort((left, right) => left.requirementId.localeCompare(right.requirementId))
|
|
84
91
|
})
|
|
85
92
|
},
|
|
86
|
-
resource:
|
|
87
|
-
.filter(([field]) => !excludedResourceFields.has(field)))),
|
|
93
|
+
resource: applicabilityResourceFacts(record, model, options),
|
|
88
94
|
systems: resources
|
|
89
95
|
.filter(({ type, id }) => type === "system" && selectedSystemIds.has(id))
|
|
90
96
|
.sort(compareRecordIds)
|
|
@@ -196,6 +202,17 @@ function compareRecordIds(left, right) {
|
|
|
196
202
|
return left.id.localeCompare(right.id);
|
|
197
203
|
}
|
|
198
204
|
|
|
205
|
+
function applicabilityResourceFacts(record, model, options) {
|
|
206
|
+
const fields = Number(model.modelVersion) >= 11 && !options.legacyResourceProjection
|
|
207
|
+
? applicabilityDecisionFields[record.type] : null;
|
|
208
|
+
if (!fields) return canonicalObject(model, record.type, Object.fromEntries(Object.entries(record)
|
|
209
|
+
.filter(([field]) => !excludedResourceFields.has(field))));
|
|
210
|
+
const value = Object.fromEntries(["id", "type", "title", "externalIds", "extensions", ...fields]
|
|
211
|
+
.filter((field) => record[field] !== undefined).map((field) => [field, record[field]]));
|
|
212
|
+
value.unavailable = ["not-applicable", "retired", "superseded"].includes(record.status);
|
|
213
|
+
return canonicalObject(model, record.type, value);
|
|
214
|
+
}
|
|
215
|
+
|
|
199
216
|
export function applicabilityReviewIsCurrent(review, record, program, resources, model, root = null) {
|
|
200
217
|
if (
|
|
201
218
|
review?.scopeRevision
|
|
@@ -204,12 +221,17 @@ export function applicabilityReviewIsCurrent(review, record, program, resources,
|
|
|
204
221
|
&& Number(model?.modelVersion || 0) < 7
|
|
205
222
|
) return true;
|
|
206
223
|
if (!review?.scopeRevision) return false;
|
|
224
|
+
if (Number(model.modelVersion) >= 11 && record?.type === "control"
|
|
225
|
+
&& (review.decision === "not-applicable") !== (record.status === "not-applicable")) return false;
|
|
226
|
+
if (Number(model.modelVersion) >= 11 && ["retired", "superseded"].includes(record?.status)) return false;
|
|
207
227
|
const matches = (revision) => revisionsMatch("applicability-scope", review.scopeRevision, revision);
|
|
208
228
|
if (matches(applicabilityScopeRevision(record, program, resources, model))) return true;
|
|
229
|
+
if (Number(model.modelVersion) >= 11 && applicabilityDecisionFields[record?.type]
|
|
230
|
+
&& matches(applicabilityScopeRevision(record, program, resources, model, { legacyResourceProjection: true }))) return true;
|
|
209
231
|
if (["commitment", "control", "complementary-control"].includes(record?.type) && matches(applicabilityScopeRevision(
|
|
210
232
|
record, program, resources, model, { legacyBroadScope: true }
|
|
211
233
|
))) return true;
|
|
212
|
-
if (
|
|
234
|
+
if (Number(model.modelVersion) >= 11 && applicabilityDecisionFields[record?.type] && root && historicalScopedReviewMatches(
|
|
213
235
|
review.scopeRevision, record, program, resources, model, root
|
|
214
236
|
)) return true;
|
|
215
237
|
// Older reviews hashed Policy approval metadata alongside scope facts.
|
|
@@ -263,9 +285,14 @@ function historicalScopedReviewMatches(stored, record, program, resources, model
|
|
|
263
285
|
if (!revisionsMatch("applicability-scope", current, applicabilityScopeRevision(
|
|
264
286
|
historicalRecord, historicalProgram, historicalResources, historicalModel
|
|
265
287
|
))) continue;
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
288
|
+
for (const legacyBroadScope of [false, true]) {
|
|
289
|
+
for (const legacyResourceProjection of [false, true]) {
|
|
290
|
+
if (revisionsMatch("applicability-scope", stored, applicabilityScopeRevision(
|
|
291
|
+
historicalRecord, historicalProgram, historicalResources, historicalModel,
|
|
292
|
+
{ legacyBroadScope, legacyResourceProjection }
|
|
293
|
+
))) return true;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
269
296
|
}
|
|
270
297
|
return false;
|
|
271
298
|
}
|
package/src/batch-review.js
CHANGED
|
@@ -142,16 +142,23 @@ function planApplicabilityReviewWithContext(context, options) {
|
|
|
142
142
|
}
|
|
143
143
|
: program;
|
|
144
144
|
const update = reviewedDecisions.flatMap(({ record, result, rationale, reviewedByIds, reviewedOn }) => {
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
rationale,
|
|
151
|
-
reviewedByIds,
|
|
152
|
-
reviewedOn,
|
|
153
|
-
scopeRevision
|
|
145
|
+
const next = { ...record };
|
|
146
|
+
if (record.type === "control" && result === "not-applicable" && record.status !== "not-applicable") {
|
|
147
|
+
next.status = "not-applicable";
|
|
148
|
+
if (loaded.model.resources.control.fields.statusTransition) {
|
|
149
|
+
next.statusTransition = { changedByIds: reviewedByIds, changedOn: reviewedOn, reason: rationale };
|
|
154
150
|
}
|
|
151
|
+
}
|
|
152
|
+
if (record.type === "control" && result !== "not-applicable" && record.status === "not-applicable") {
|
|
153
|
+
next.status = "planned";
|
|
154
|
+
delete next.statusTransition;
|
|
155
|
+
}
|
|
156
|
+
next.applicabilityReview = {
|
|
157
|
+
decision: result,
|
|
158
|
+
rationale,
|
|
159
|
+
reviewedByIds,
|
|
160
|
+
reviewedOn,
|
|
161
|
+
scopeRevision: applicabilityScopeRevision(next, reviewedProgram, loaded.resources, loaded.model)
|
|
155
162
|
};
|
|
156
163
|
if (record.type === "requirement") {
|
|
157
164
|
if (modelSupports(loaded.model, "program-scope")) {
|
|
@@ -160,8 +167,6 @@ function planApplicabilityReviewWithContext(context, options) {
|
|
|
160
167
|
next.applicability = result;
|
|
161
168
|
next.applicabilityRationale = rationale;
|
|
162
169
|
}
|
|
163
|
-
if (record.type === "control" && result === "not-applicable") next.status = "not-applicable";
|
|
164
|
-
if (record.type === "control" && result === "applicable" && record.status === "not-applicable") next.status = "planned";
|
|
165
170
|
return [next];
|
|
166
171
|
});
|
|
167
172
|
if (v4RequirementDecisions.length) {
|
package/src/collection-scope.js
CHANGED
|
@@ -200,6 +200,7 @@ function scopedProgramPeople(loaded, program = {}) {
|
|
|
200
200
|
|
|
201
201
|
export function collectionRevisionInputs(loaded, resourceType, program, options = {}) {
|
|
202
202
|
const legacy = options.legacy === true;
|
|
203
|
+
const currentModel = modelSupports(loaded.model, "retention-schedule-approval");
|
|
203
204
|
const reviewed = scopedCollectionRecords(loaded, resourceType, program);
|
|
204
205
|
if (!modelSupports(loaded.model, "program-scope")) {
|
|
205
206
|
return reviewed.map((record) => ({ record, value: record, includeContent: true }));
|
|
@@ -367,11 +368,11 @@ export function collectionRevisionInputs(loaded, resourceType, program, options
|
|
|
367
368
|
&& record.type === "document"
|
|
368
369
|
? retentionScheduleDocumentProposalValue(record)
|
|
369
370
|
: options.historicalReviewMetadata
|
|
370
|
-
? dependencyRevisionValue(resourceType, record, legacy)
|
|
371
|
-
: substantiveRevisionValue(dependencyRevisionValue(resourceType, record, legacy)),
|
|
371
|
+
? dependencyRevisionValue(resourceType, record, legacy, true, currentModel)
|
|
372
|
+
: substantiveRevisionValue(dependencyRevisionValue(resourceType, record, legacy, false, currentModel)),
|
|
372
373
|
includeContent: legacy
|
|
373
374
|
|| reviewedIds.has(record.id)
|
|
374
|
-
|| dependencyContentAffectsRevision(resourceType, record.type)
|
|
375
|
+
|| dependencyContentAffectsRevision(resourceType, record.type, options.historicalReviewMetadata, currentModel)
|
|
375
376
|
}));
|
|
376
377
|
}
|
|
377
378
|
|
|
@@ -379,7 +380,7 @@ function retentionScheduleRowProposalValue(record, historicalReviewMetadata = fa
|
|
|
379
380
|
return Object.fromEntries(Object.entries(record).filter(([field]) => ![
|
|
380
381
|
"approvedByIds",
|
|
381
382
|
"approvedOn",
|
|
382
|
-
...(historicalReviewMetadata ? [] : ["reviewedSourceRevisions"])
|
|
383
|
+
...(historicalReviewMetadata ? [] : ["reviewedSourceRevisions", "tags", "statusTransition"])
|
|
383
384
|
].includes(field)));
|
|
384
385
|
}
|
|
385
386
|
|
|
@@ -591,22 +592,31 @@ const dependencyContentTypes = {
|
|
|
591
592
|
component: new Set(["system"]),
|
|
592
593
|
control: new Set(["system", "component", "obligation", "source-coverage", "evidence", "retention-schedule-item", "document", "policy", "training"]),
|
|
593
594
|
"retention-schedule-item": new Set(["document", "policy"]),
|
|
594
|
-
"complementary-control": new Set(["system", "
|
|
595
|
+
"complementary-control": new Set(["system", "document", "component"])
|
|
595
596
|
};
|
|
596
597
|
|
|
597
|
-
function dependencyRevisionValue(resourceType, record, legacy) {
|
|
598
|
-
const
|
|
598
|
+
function dependencyRevisionValue(resourceType, record, legacy, historicalReviewMetadata = false, currentModel = false) {
|
|
599
|
+
const controlDecision = currentModel && !historicalReviewMetadata && record.type === "control"
|
|
600
|
+
&& ["retention-schedule-item", "complementary-control"].includes(resourceType);
|
|
601
|
+
const fields = controlDecision
|
|
602
|
+
? resourceType === "retention-schedule-item"
|
|
603
|
+
? ["id", "type", "statement", "activity", "controlType", "operationMode", "operationPattern", "requirementIds", "systemIds", "policyIds", "componentIds", "evidenceSourceComponentIds"]
|
|
604
|
+
: ["id", "type", "statement", "activity", "controlType", "operationMode", "operationPattern", "requirementIds", "systemIds", "policyIds", "componentIds", "evidenceSourceComponentIds"]
|
|
605
|
+
: legacy
|
|
599
606
|
? legacyDependencyFieldOverrides[resourceType]?.[record.type]
|
|
600
607
|
|| dependencyFields[resourceType]?.[record.type]
|
|
601
608
|
: dependencyFields[resourceType]?.[record.type];
|
|
602
609
|
if (!fields) return { id: record.id, type: record.type };
|
|
603
|
-
|
|
610
|
+
const value = Object.fromEntries(fields
|
|
604
611
|
.filter((field) => record[field] !== undefined)
|
|
605
612
|
.map((field) => [field, record[field]]));
|
|
613
|
+
if (controlDecision) value.unavailable = ["not-applicable", "retired", "superseded"].includes(record.status);
|
|
614
|
+
return value;
|
|
606
615
|
}
|
|
607
616
|
|
|
608
|
-
function dependencyContentAffectsRevision(resourceType, dependencyType) {
|
|
609
|
-
return
|
|
617
|
+
function dependencyContentAffectsRevision(resourceType, dependencyType, historicalReviewMetadata = false, currentModel = false) {
|
|
618
|
+
return ((!currentModel || historicalReviewMetadata) && resourceType === "complementary-control" && dependencyType === "control")
|
|
619
|
+
|| dependencyContentTypes[resourceType]?.has(dependencyType) || false;
|
|
610
620
|
}
|
|
611
621
|
|
|
612
622
|
function sorted(values) {
|
package/src/retention.js
CHANGED
|
@@ -167,7 +167,7 @@ export async function resourceReviewRevisions(loaded, ids, scopeHashInput = "leg
|
|
|
167
167
|
if (!entry || reviewing.has(id)) return null;
|
|
168
168
|
reviewing.add(id);
|
|
169
169
|
const parts = [reviewSource(loaded, entry, scopeHashInput)];
|
|
170
|
-
for (const markdown of
|
|
170
|
+
for (const markdown of reviewMarkdownEntries(loaded, entry, false)) {
|
|
171
171
|
try {
|
|
172
172
|
parts.push(await readFile(resolveDataPath(loaded.root, markdown.path), "utf8"));
|
|
173
173
|
} catch (error) {
|
|
@@ -200,7 +200,7 @@ export function resourceReviewRevisionsSync(loaded, ids, scopeHashInput = "legac
|
|
|
200
200
|
if (!entry || reviewing.has(id)) return null;
|
|
201
201
|
reviewing.add(id);
|
|
202
202
|
const parts = [reviewSource(loaded, entry, scopeHashInput, legacySource)];
|
|
203
|
-
for (const markdown of
|
|
203
|
+
for (const markdown of reviewMarkdownEntries(loaded, entry, legacySource)) {
|
|
204
204
|
try {
|
|
205
205
|
const source = historicalCommit
|
|
206
206
|
? loaded.historicalFiles
|
|
@@ -317,10 +317,52 @@ function reviewSource(loaded, entry, scopeHashInput = "legacy", legacySource = f
|
|
|
317
317
|
}
|
|
318
318
|
return legacySource
|
|
319
319
|
? canonicalCalculatedRevisionJson(entry.source, scopeHashInput)
|
|
320
|
-
: canonicalCalculatedRevisionJson(JSON.stringify(substantiveReviewSource(
|
|
320
|
+
: canonicalCalculatedRevisionJson(JSON.stringify(substantiveReviewSource(
|
|
321
|
+
JSON.parse(entry.source), modelSupports(loaded.model, "retention-schedule-approval")
|
|
322
|
+
)), scopeHashInput);
|
|
321
323
|
}
|
|
322
324
|
|
|
323
|
-
function
|
|
325
|
+
function reviewMarkdownEntries(loaded, entry, legacySource) {
|
|
326
|
+
// Control Markdown records the implementation procedure. Retention and
|
|
327
|
+
// mapping decisions depend on the structured Control design instead.
|
|
328
|
+
if (!legacySource && modelSupports(loaded.model, "retention-schedule-approval")
|
|
329
|
+
&& entry.record.type === "control") return [];
|
|
330
|
+
return markdownEntries(loaded.model, entry.record);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const reviewDecisionFields = {
|
|
334
|
+
control: ["statement", "requirementIds", "code", "activity", "controlType", "operationMode", "operationPattern", "systemIds", "policyIds", "componentIds", "evidenceSourceComponentIds"],
|
|
335
|
+
commitment: ["commitmentKind", "statement", "systemIds", "sourceResourceIds", "requirementIds", "controlIds", "customerFacing", "effectiveOn", "reportingRouteRequirements"],
|
|
336
|
+
"complementary-control": ["responsibleParty", "statement", "systemIds", "vendorId", "requirementIds", "commitmentIds", "relatedControlIds", "sourceDocumentIds", "componentIds"],
|
|
337
|
+
"source-coverage": ["sourceFamilyId", "coverageKind", "scopeResourceIds", "excludedPopulation", "collectionCadence", "reconciliationMethod", "validFrom", "validThrough", "componentId", "retentionScheduleItemIds"],
|
|
338
|
+
system: ["purpose", "servicesProvided", "boundary", "exclusions", "criticality", "informationTypeIds", "classificationId", "internetExposed", "continuityObjectives"],
|
|
339
|
+
component: ["componentKind", "description", "criticality", "environment", "vendorId", "systemUses", "informationUses", "evidenceSourceKinds", "internetExposed", "classificationId", "continuityObjectives"],
|
|
340
|
+
vendor: ["category", "criticality", "description", "standardAgreement", "agreementDocumentId", "startDate", "endDate", "informationTypeIds", "classificationId"],
|
|
341
|
+
"information-type": ["classificationId", "description"],
|
|
342
|
+
framework: ["version", "publisher", "description", "sourceReference", "effectiveOn"],
|
|
343
|
+
requirement: ["frameworkId", "reference", "description", "parentRequirementId"],
|
|
344
|
+
policy: ["policyNumber", "policyKind", "version", "effectiveOn", "supersedesId", "parentPolicyId", "relatedPolicyIds", "relatedDocumentIds", "requirementIds", "audience", "acknowledgementRequired", "programRole", "reportingRouteRequirements"],
|
|
345
|
+
document: ["documentKind", "workflowScope", "template", "version", "effectiveOn", "supersedesId", "systemIds", "controlIds", "relatedDocumentIds", "audience", "acknowledgementRequired", "trainingIds", "classificationId", "programRole", "componentIds", "reportingRouteRequirements"]
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
function substantiveReviewSource(record, currentModel) {
|
|
349
|
+
if (currentModel && reviewDecisionFields[record.type]) {
|
|
350
|
+
const fields = ["id", "type", "title", "extensions", "externalIds", ...reviewDecisionFields[record.type]];
|
|
351
|
+
const decision = Object.fromEntries(fields.filter((field) => record[field] !== undefined)
|
|
352
|
+
.map((field) => [field, record[field]]));
|
|
353
|
+
if (record.type === "control") {
|
|
354
|
+
decision.unavailable = ["not-applicable", "retired", "superseded"].includes(record.status);
|
|
355
|
+
} else if (["policy", "document"].includes(record.type)) {
|
|
356
|
+
decision.governingStatus = ["approved", "active"].includes(record.status) ? "approved" : record.status || null;
|
|
357
|
+
} else {
|
|
358
|
+
decision.status = record.status || null;
|
|
359
|
+
}
|
|
360
|
+
if (["control", "commitment", "complementary-control"].includes(record.type)) {
|
|
361
|
+
decision.nonApplicableDecision = ["not-applicable", "externally-managed", "zero-population"]
|
|
362
|
+
.includes(record.applicabilityReview?.decision) ? record.applicabilityReview.decision : null;
|
|
363
|
+
}
|
|
364
|
+
return decision;
|
|
365
|
+
}
|
|
324
366
|
const source = record.type === "program" ? {
|
|
325
367
|
id: record.id,
|
|
326
368
|
type: record.type,
|