filegrc 0.3.4 → 0.4.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 +16 -6
- package/model/index.js +37 -3
- package/model/v1.json +81 -47
- package/model/v2.json +8022 -0
- package/package.json +1 -1
- package/src/agent.js +36 -8
- package/src/audit-preparation.js +63 -60
- package/src/cli.js +168 -110
- package/src/coverage.js +50 -0
- package/src/evidence-packet.js +115 -75
- package/src/files.js +230 -28
- package/src/git.js +239 -41
- package/src/index.js +5 -5
- package/src/model-docs.js +88 -7
- package/src/model-migration.js +1463 -0
- package/src/mutation.js +42 -0
- package/src/obligations.js +108 -84
- package/src/parties.js +17 -2
- package/src/program-path.js +31 -58
- package/src/program-readiness.js +142 -106
- package/src/resource-status.js +17 -0
- package/src/server.js +110 -39
- package/src/setup.js +27 -28
- package/src/state.js +86 -25
- package/src/timing.js +41 -0
- package/src/validate.js +609 -43
- package/src/web.js +506 -129
- package/src/workspace.js +15 -7
- package/src/evidence-tests.js +0 -69
package/src/program-readiness.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { coverageEnd, coverageStart } from "./coverage.js";
|
|
2
3
|
import { planObligations } from "./obligations.js";
|
|
3
4
|
import { resolveDataPath } from "./paths.js";
|
|
4
5
|
import { obligationIsRunning } from "./program-lifecycle.js";
|
|
@@ -7,8 +8,6 @@ import { markdownEntries } from "./resource-markdown.js";
|
|
|
7
8
|
import { currentCalendarDate } from "./time.js";
|
|
8
9
|
import { loadWorkspace } from "./workspace.js";
|
|
9
10
|
|
|
10
|
-
const TEST_EVIDENCE_KINDS = new Set(["test-capture", "test-export"]);
|
|
11
|
-
|
|
12
11
|
export async function assessProgramReadiness(input, options = {}) {
|
|
13
12
|
const loaded = input?.resources && input?.model && input?.entries
|
|
14
13
|
? input
|
|
@@ -25,38 +24,33 @@ export async function assessProgramReadiness(input, options = {}) {
|
|
|
25
24
|
return markdown.get(record.id);
|
|
26
25
|
};
|
|
27
26
|
|
|
27
|
+
const controlStage = await controlsStage(scope, byId, readMarkdown, asOf);
|
|
28
28
|
const sourceStage = await evidenceSourcesStage(scope, byId, loaded.model, readMarkdown);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"evidence",
|
|
32
|
-
"Test Evidence Collection",
|
|
33
|
-
"For external evidence without a dedicated Step 5 record, catalog the authoritative Systems, document repeatable extraction, and verify one test capture before operation begins.",
|
|
34
|
-
[...sourceStage.items, ...collectionStage.items]
|
|
35
|
-
);
|
|
29
|
+
controlStage.items.push(...sourceStage.items);
|
|
30
|
+
controlStage.description = "Each implemented control needs an owner, actual procedure, scope, operation pattern, mappings, an implementation date, and complete authoritative source Systems with the required evidence roles, access owners, and retrieval instructions.";
|
|
36
31
|
const evidenceGateStages = [
|
|
37
32
|
scopeStage(workspace, scope, records, byId),
|
|
38
33
|
await policiesStage(scope, byId, readMarkdown, asOf),
|
|
39
|
-
|
|
40
|
-
evidenceStage
|
|
34
|
+
controlStage
|
|
41
35
|
];
|
|
42
36
|
for (const current of evidenceGateStages) finalizeStage(current);
|
|
43
37
|
const evidenceReady = evidenceGateStages.every((current) => current.counts.action === 0);
|
|
44
38
|
const stages = [
|
|
45
39
|
...evidenceGateStages,
|
|
46
|
-
operationStage(workspace, scope, records, byId, asOf, evidenceReady)
|
|
40
|
+
operationStage(workspace, scope, records, byId, asOf, evidenceReady, loaded.model)
|
|
47
41
|
];
|
|
48
42
|
finalizeStage(stages.at(-1));
|
|
49
43
|
const candidateStarted = Boolean(
|
|
50
44
|
workspace?.assuranceGoal === "soc-2-type-2"
|
|
51
|
-
&& workspace.
|
|
52
|
-
&& workspace.
|
|
45
|
+
&& workspace.candidateCoverage?.kind === "range"
|
|
46
|
+
&& coverageStart(workspace.candidateCoverage) <= asOf
|
|
53
47
|
);
|
|
54
|
-
const obligations = planObligations(records, { asOf, through: asOf });
|
|
48
|
+
const obligations = planObligations(records, { asOf, through: asOf, model: loaded.model });
|
|
55
49
|
const operating = evidenceReady && candidateStarted && obligations.counts.overdue === 0;
|
|
56
50
|
const canStartCandidatePeriod = Boolean(
|
|
57
51
|
evidenceReady
|
|
58
52
|
&& workspace?.assuranceGoal === "soc-2-type-2"
|
|
59
|
-
&& !workspace.
|
|
53
|
+
&& !workspace.candidateCoverage
|
|
60
54
|
);
|
|
61
55
|
const items = stages.flatMap((current) => current.items);
|
|
62
56
|
const managedItems = items.filter((current) => !["info", "later"].includes(current.status));
|
|
@@ -70,9 +64,7 @@ export async function assessProgramReadiness(input, options = {}) {
|
|
|
70
64
|
target: {
|
|
71
65
|
goal: workspace?.assuranceGoal || "none",
|
|
72
66
|
label: assuranceGoalLabel(workspace?.assuranceGoal),
|
|
73
|
-
|
|
74
|
-
candidatePeriodStart: workspace?.candidatePeriodStart || null,
|
|
75
|
-
candidatePeriodEnd: workspace?.candidatePeriodEnd || null
|
|
67
|
+
candidateCoverage: workspace?.candidateCoverage || null
|
|
76
68
|
},
|
|
77
69
|
status: operating ? "operating" : evidenceReady ? "evidence-ready" : "needs-work",
|
|
78
70
|
evidenceReady,
|
|
@@ -96,15 +88,37 @@ export async function assessProgramReadiness(input, options = {}) {
|
|
|
96
88
|
};
|
|
97
89
|
}
|
|
98
90
|
|
|
91
|
+
export async function assessEvidenceMap(input, options = {}) {
|
|
92
|
+
const readiness = await assessProgramReadiness(input, options);
|
|
93
|
+
const evidenceItems = readiness.stages
|
|
94
|
+
.find((stage) => stage.id === "controls")
|
|
95
|
+
?.items.filter((current) => current.id.startsWith("source-family-")) || [];
|
|
96
|
+
const counts = countStatuses(evidenceItems);
|
|
97
|
+
return {
|
|
98
|
+
schemaVersion: 1,
|
|
99
|
+
generatedAt: readiness.generatedAt,
|
|
100
|
+
asOf: readiness.asOf,
|
|
101
|
+
status: counts.action ? "action" : "complete",
|
|
102
|
+
counts,
|
|
103
|
+
workflow: [
|
|
104
|
+
"Choose an existing System or create the System that is authoritative for each evidence family.",
|
|
105
|
+
"On every source System, set an evidence source role, name current evidence access owners, and write repeatable retrieval instructions in Record Markdown.",
|
|
106
|
+
"Set each selected Control's evidenceSourceIds to the authoritative Systems that produce its evidence.",
|
|
107
|
+
"Run program-readiness again and resolve every incomplete source check and control mapping before marking the Controls implemented."
|
|
108
|
+
],
|
|
109
|
+
items: evidenceItems
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
99
113
|
function programScope(workspace, records, byId) {
|
|
100
114
|
const select = (ids, type, fallback) => {
|
|
101
115
|
if (ids?.length) return ids.map((id) => byId.get(id)).filter((record) => record?.type === type);
|
|
102
116
|
return records.filter(fallback);
|
|
103
117
|
};
|
|
104
118
|
return {
|
|
105
|
-
systems:
|
|
106
|
-
|
|
107
|
-
|
|
119
|
+
systems: (workspace?.systemIds || [])
|
|
120
|
+
.map((id) => byId.get(id))
|
|
121
|
+
.filter((record) => record?.type === "system" && record.status !== "retired"),
|
|
108
122
|
frameworks: select(workspace?.frameworkIds, "framework", (record) => (
|
|
109
123
|
record.type === "framework" && record.status === "active"
|
|
110
124
|
)),
|
|
@@ -134,9 +148,8 @@ function scopeStage(workspace, scope, records, byId) {
|
|
|
134
148
|
|
|
135
149
|
const completeSystems = scope.systems.filter((system) => (
|
|
136
150
|
system.status === "active"
|
|
137
|
-
&& system.inScope === true
|
|
138
151
|
&& system.description
|
|
139
|
-
&& system.
|
|
152
|
+
&& system.classificationId
|
|
140
153
|
&& (system.ownerIds || []).length
|
|
141
154
|
));
|
|
142
155
|
items.push(item(
|
|
@@ -188,6 +201,9 @@ function programOwnershipItem(records, byId) {
|
|
|
188
201
|
));
|
|
189
202
|
const unresolved = ownedRecords.filter((record) => currentPartyPeople(record.ownerIds, byId).size === 0);
|
|
190
203
|
const currentOwners = new Set(ownedRecords.flatMap((record) => [...currentPartyPeople(record.ownerIds, byId)]));
|
|
204
|
+
const missingJobTitles = [...currentOwners]
|
|
205
|
+
.map((id) => byId.get(id))
|
|
206
|
+
.filter((record) => record?.type === "person" && record.status === "active" && !String(record.jobTitle || "").trim());
|
|
191
207
|
const oversight = byId.get("team-security-risk-oversight");
|
|
192
208
|
const policyOwnerIds = new Set(records
|
|
193
209
|
.filter((record) => record.type === "policy" && !["retired", "superseded"].includes(record.status))
|
|
@@ -202,7 +218,10 @@ function programOwnershipItem(records, byId) {
|
|
|
202
218
|
&& oversightChairs.size > 0
|
|
203
219
|
&& ![...oversightChairs].some((id) => policyOwnerIds.has(id))
|
|
204
220
|
);
|
|
205
|
-
const complete = currentOwners.size > 0
|
|
221
|
+
const complete = currentOwners.size > 0
|
|
222
|
+
&& unresolved.length === 0
|
|
223
|
+
&& missingJobTitles.length === 0
|
|
224
|
+
&& oversightComplete;
|
|
206
225
|
const unresolvedAssignments = unresolved.map((record) => ({
|
|
207
226
|
resourceType: record.type,
|
|
208
227
|
resourceId: record.id,
|
|
@@ -215,6 +234,9 @@ function programOwnershipItem(records, byId) {
|
|
|
215
234
|
if (unresolved.length) {
|
|
216
235
|
detail.push(`${unresolved.length} ${unresolved.length === 1 ? "record has" : "records have"} no current person owner.`);
|
|
217
236
|
}
|
|
237
|
+
if (missingJobTitles.length) {
|
|
238
|
+
detail.push(`${missingJobTitles.length} active ${missingJobTitles.length === 1 ? "owner needs" : "owners need"} an organizational job title.`);
|
|
239
|
+
}
|
|
218
240
|
if (!oversightComplete) detail.push("Finish and activate Security and Risk Oversight with a current chair who is separate from policy ownership.");
|
|
219
241
|
const oversightId = oversight?.id ? shellArgument(oversight.id) : null;
|
|
220
242
|
return item(
|
|
@@ -227,9 +249,11 @@ function programOwnershipItem(records, byId) {
|
|
|
227
249
|
!oversightComplete ? oversight : unresolved[0] || { type: "person" },
|
|
228
250
|
{
|
|
229
251
|
unresolvedAssignments,
|
|
252
|
+
missingJobTitleIds: missingJobTitles.map(({ id }) => id),
|
|
230
253
|
...(!oversightComplete && oversight ? {
|
|
231
254
|
commands: [
|
|
232
255
|
"npx filegrc guide person --json",
|
|
256
|
+
"npx filegrc guide appointment --json",
|
|
233
257
|
"npx filegrc list person --json",
|
|
234
258
|
'npx filegrc scaffold person --title "REVIEWER NAME" | npx filegrc create - --json',
|
|
235
259
|
`npx filegrc get ${oversightId} --mutation`,
|
|
@@ -251,8 +275,13 @@ function ownershipResolutionReasons(ownerIds, byId) {
|
|
|
251
275
|
if (owner.type === "team" && owner.status !== "active") {
|
|
252
276
|
return [{ ownerId, reason: "inactive-team" }];
|
|
253
277
|
}
|
|
278
|
+
if (owner.type === "appointment" && owner.status !== "active") {
|
|
279
|
+
return [{ ownerId, reason: "inactive-appointment" }];
|
|
280
|
+
}
|
|
254
281
|
if (currentPartyPeople([ownerId], byId).size === 0) {
|
|
255
|
-
|
|
282
|
+
if (owner.type === "team") return [{ ownerId, reason: "team-has-no-current-members" }];
|
|
283
|
+
if (owner.type === "appointment") return [{ ownerId, reason: "appointment-has-no-current-holder" }];
|
|
284
|
+
return [{ ownerId, reason: "no-current-person" }];
|
|
256
285
|
}
|
|
257
286
|
return [];
|
|
258
287
|
});
|
|
@@ -312,7 +341,7 @@ async function policiesStage(scope, byId, readMarkdown, asOf) {
|
|
|
312
341
|
&& policy.approvedOn
|
|
313
342
|
&& partiesIndependent(policy.ownerIds, policy.approverIds, byId),
|
|
314
343
|
effective: policy.status === "active" && policy.effectiveOn && policy.effectiveOn <= asOf,
|
|
315
|
-
linkedControls:
|
|
344
|
+
linkedControls: scope.controls.some((control) => (control.policyIds || []).includes(policy.id)),
|
|
316
345
|
contentComplete: Boolean(source.trim()) && placeholderCount === 0
|
|
317
346
|
};
|
|
318
347
|
const missing = Object.entries(checks).filter(([, value]) => !value).map(([name]) => policyCheckLabel(name));
|
|
@@ -348,13 +377,14 @@ async function controlsStage(scope, byId, readMarkdown, asOf) {
|
|
|
348
377
|
owner: (control.ownerIds || []).length > 0,
|
|
349
378
|
procedure: substantiveMarkdown(source) && openPlaceholderCount(source) === 0,
|
|
350
379
|
scope: (control.systemIds || []).some((id) => scope.systems.some((system) => system.id === id)),
|
|
351
|
-
|
|
380
|
+
operationPattern: Boolean(control.operationPattern),
|
|
352
381
|
evidenceSource: sourceSystems.length > 0,
|
|
353
382
|
implementationDate: Boolean(control.effectiveOn && control.effectiveOn <= asOf),
|
|
354
383
|
policyMapping: (control.policyIds || []).length > 0,
|
|
355
384
|
criteriaMapping: (control.requirementIds || []).length > 0,
|
|
356
|
-
...(
|
|
357
|
-
workQueue: queueSchedules.
|
|
385
|
+
...(["scheduled", "event-driven", "mixed"].includes(control.operationPattern) ? {
|
|
386
|
+
workQueue: queueSchedules.length > 0
|
|
387
|
+
&& queueSchedules.every((obligation) => obligationIsRunning(obligation, byId, asOf))
|
|
358
388
|
} : {})
|
|
359
389
|
};
|
|
360
390
|
const missing = Object.entries(checks).filter(([, value]) => !value).map(([name]) => controlCheckLabel(name));
|
|
@@ -375,36 +405,68 @@ async function controlsStage(scope, byId, readMarkdown, asOf) {
|
|
|
375
405
|
}
|
|
376
406
|
));
|
|
377
407
|
}
|
|
378
|
-
return stage("controls", "Implement Controls", "Each implemented control needs an owner, actual procedure, scope,
|
|
408
|
+
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 running.", items);
|
|
379
409
|
}
|
|
380
410
|
|
|
381
411
|
async function evidenceSourcesStage(scope, byId, model, readMarkdown) {
|
|
382
|
-
const families = selectedControlFamilies(scope.controls, model)
|
|
412
|
+
const families = selectedControlFamilies(scope.controls, model);
|
|
383
413
|
const items = [];
|
|
384
414
|
for (const family of families) {
|
|
385
415
|
const selectedSources = [...new Set(family.controls.flatMap((control) => control.evidenceSourceIds || []))]
|
|
386
416
|
.map((id) => byId.get(id))
|
|
387
417
|
.filter((record) => record?.type === "system");
|
|
388
418
|
const completeSources = [];
|
|
419
|
+
const sourceSystemChecks = [];
|
|
389
420
|
for (const source of selectedSources) {
|
|
390
421
|
const instructions = await readMarkdown(source);
|
|
391
422
|
const matchesRole = !family.sourceKinds.length
|
|
392
423
|
|| family.sourceKinds.some((kind) => (source.evidenceSourceKinds || []).includes(kind));
|
|
393
|
-
|
|
394
|
-
source.status === "active"
|
|
395
|
-
&&
|
|
396
|
-
|
|
397
|
-
&& (
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
424
|
+
const checks = {
|
|
425
|
+
active: source.status === "active",
|
|
426
|
+
sourceRole: matchesRole && (source.evidenceSourceKinds || []).length > 0,
|
|
427
|
+
accessOwners: (source.evidenceOwnerIds || []).length > 0,
|
|
428
|
+
retrievalInstructions: substantiveMarkdown(instructions) && openPlaceholderCount(instructions) === 0
|
|
429
|
+
};
|
|
430
|
+
const complete = Object.values(checks).every(Boolean);
|
|
431
|
+
sourceSystemChecks.push({
|
|
432
|
+
sourceSystemId: source.id,
|
|
433
|
+
complete,
|
|
434
|
+
checks
|
|
435
|
+
});
|
|
436
|
+
if (complete) {
|
|
401
437
|
completeSources.push(source);
|
|
402
438
|
}
|
|
403
439
|
}
|
|
404
|
-
const
|
|
405
|
-
(control.evidenceSourceIds || []).
|
|
406
|
-
|
|
440
|
+
const controlMappings = family.controls.map((control) => {
|
|
441
|
+
const sourceSystemIds = (control.evidenceSourceIds || []).filter((id) => selectedSources.some((source) => source.id === id));
|
|
442
|
+
const completeSourceSystemIds = sourceSystemIds.filter((id) => completeSources.some((source) => source.id === id));
|
|
443
|
+
return {
|
|
444
|
+
controlId: control.id,
|
|
445
|
+
sourceSystemIds,
|
|
446
|
+
completeSourceSystemIds,
|
|
447
|
+
mapped: sourceSystemIds.length > 0,
|
|
448
|
+
complete: completeSourceSystemIds.length > 0
|
|
449
|
+
};
|
|
450
|
+
});
|
|
451
|
+
const coveredControls = controlMappings.filter(({ complete }) => complete);
|
|
407
452
|
const complete = completeSources.length > 0 && coveredControls.length === family.controls.length;
|
|
453
|
+
const commands = [
|
|
454
|
+
...(selectedSources.length
|
|
455
|
+
? sourceSystemChecks.filter(({ complete: sourceComplete }) => !sourceComplete).flatMap(({ sourceSystemId }) => [
|
|
456
|
+
`npx filegrc get ${shellArgument(sourceSystemId)} --mutation > /tmp/${shellArgument(sourceSystemId)}.json`,
|
|
457
|
+
`npx filegrc update system ${shellArgument(sourceSystemId)} /tmp/${shellArgument(sourceSystemId)}.json --json`
|
|
458
|
+
])
|
|
459
|
+
: [
|
|
460
|
+
"npx filegrc list system --json",
|
|
461
|
+
'npx filegrc scaffold system --title "SYSTEM NAME" > /tmp/filegrc-system.json',
|
|
462
|
+
"npx filegrc create /tmp/filegrc-system.json --json"
|
|
463
|
+
]),
|
|
464
|
+
...controlMappings.filter(({ mapped }) => !mapped).flatMap(({ controlId }) => [
|
|
465
|
+
`npx filegrc get ${shellArgument(controlId)} --mutation > /tmp/${shellArgument(controlId)}.json`,
|
|
466
|
+
`npx filegrc update control ${shellArgument(controlId)} /tmp/${shellArgument(controlId)}.json --json`
|
|
467
|
+
]),
|
|
468
|
+
"npx filegrc program-readiness --json"
|
|
469
|
+
];
|
|
408
470
|
items.push(item(
|
|
409
471
|
`source-family-${family.id}`,
|
|
410
472
|
complete ? "complete" : "action",
|
|
@@ -413,57 +475,27 @@ async function evidenceSourcesStage(scope, byId, model, readMarkdown) {
|
|
|
413
475
|
? `${completeSources.map((source) => source.title).join(", ")} cover all ${family.controls.length} selected controls and record access owners and extraction instructions.`
|
|
414
476
|
: `${coveredControls.length} of ${family.controls.length} selected controls have an active authoritative system with the required source role, access owners, and extraction instructions.`,
|
|
415
477
|
completeSources[0] || selectedSources[0] || { type: "system" },
|
|
416
|
-
{ controlIds: family.controls.map((control) => control.id), sourceSystemIds: selectedSources.map((source) => source.id) }
|
|
417
|
-
));
|
|
418
|
-
}
|
|
419
|
-
return stage("sources", "Configure Evidence Sources", "Catalog the authoritative systems, name who can export from them, and write repeatable extraction instructions.", items);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function evidenceCollectionStage(scope, records, byId, model) {
|
|
423
|
-
const families = selectedControlFamilies(scope.controls, model).filter(requiresCollectionTest);
|
|
424
|
-
const captures = records.filter((record) => (
|
|
425
|
-
record.type === "evidence"
|
|
426
|
-
&& TEST_EVIDENCE_KINDS.has(record.evidenceKind)
|
|
427
|
-
));
|
|
428
|
-
const items = families.map((family) => {
|
|
429
|
-
const configuredSourceIds = new Set(family.controls.flatMap((control) => control.evidenceSourceIds || []));
|
|
430
|
-
const controlIds = new Set(family.controls.map((control) => control.id));
|
|
431
|
-
const capture = captures.find((record) => record.collectionTestFamilyId === family.id)
|
|
432
|
-
|| captures.find((record) => (
|
|
433
|
-
[...controlIdsForRecord(record, byId)].some((id) => controlIds.has(id))
|
|
434
|
-
));
|
|
435
|
-
const sourceIds = new Set([
|
|
436
|
-
...configuredSourceIds,
|
|
437
|
-
...(capture?.sourceSystemId ? [capture.sourceSystemId] : [])
|
|
438
|
-
]);
|
|
439
|
-
const verified = capture?.status === "verified";
|
|
440
|
-
return item(
|
|
441
|
-
`test-family-${family.id}`,
|
|
442
|
-
verified ? "complete" : "action",
|
|
443
|
-
family.title,
|
|
444
|
-
verified
|
|
445
|
-
? `${capture.title} proves that management successfully captured and verified evidence from ${byId.get(capture.sourceSystemId)?.title || "the authoritative source"}.`
|
|
446
|
-
: capture?.status === "draft"
|
|
447
|
-
? `${capture.title} is a draft. Open it, select the authoritative source System, collect the named artifact, and have another person verify it.`
|
|
448
|
-
: capture
|
|
449
|
-
? `${capture.title} is ${capture.status} but must be verified before this family is ready.`
|
|
450
|
-
: `Run and verify one test export or test capture from an authoritative source outside filegrc, then link it to a family control.`,
|
|
451
|
-
capture || { type: "evidence" },
|
|
452
478
|
{
|
|
453
479
|
familyId: family.id,
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
480
|
+
sourceKinds: family.sourceKinds,
|
|
481
|
+
controlIds: family.controls.map((control) => control.id),
|
|
482
|
+
sourceSystemIds: selectedSources.map((source) => source.id),
|
|
483
|
+
completeSourceSystemIds: completeSources.map((source) => source.id),
|
|
484
|
+
sourceSystemChecks,
|
|
485
|
+
controlMappings,
|
|
486
|
+
evidenceForm: family.evidenceForm,
|
|
487
|
+
evidencePrompt: family.evidencePrompt,
|
|
488
|
+
description: family.description,
|
|
489
|
+
timing: family.timing,
|
|
490
|
+
operationRecordTypes: family.operationRecordTypes,
|
|
491
|
+
commands
|
|
460
492
|
}
|
|
461
|
-
);
|
|
462
|
-
}
|
|
463
|
-
return stage("
|
|
493
|
+
));
|
|
494
|
+
}
|
|
495
|
+
return stage("sources", "Control Evidence Sources", "Complete the authoritative Systems for every selected control family before marking the Controls implemented.", items);
|
|
464
496
|
}
|
|
465
497
|
|
|
466
|
-
function operationStage(workspace, scope, records, byId, asOf, evidenceReady) {
|
|
498
|
+
function operationStage(workspace, scope, records, byId, asOf, evidenceReady, model) {
|
|
467
499
|
const goal = workspace?.assuranceGoal || "none";
|
|
468
500
|
if (!evidenceReady) {
|
|
469
501
|
return stage("operation", "Operate the Program", "Run the controls and preserve dated evidence after the Evidence Ready gate passes.", [
|
|
@@ -471,13 +503,15 @@ function operationStage(workspace, scope, records, byId, asOf, evidenceReady) {
|
|
|
471
503
|
"operation-later",
|
|
472
504
|
"later",
|
|
473
505
|
"Begin reliable evidence collection",
|
|
474
|
-
"Finish scope, policy adoption, control implementation,
|
|
506
|
+
"Finish scope, policy adoption, and control implementation, including complete authoritative evidence sources, before recording the candidate period.",
|
|
475
507
|
workspace || { type: "workspace" }
|
|
476
508
|
)
|
|
477
509
|
]);
|
|
478
510
|
}
|
|
479
511
|
if (goal !== "soc-2-type-2") {
|
|
480
|
-
const date = workspace?.
|
|
512
|
+
const date = workspace?.candidateCoverage?.kind === "as-of"
|
|
513
|
+
? workspace.candidateCoverage.on
|
|
514
|
+
: null;
|
|
481
515
|
return stage("operation", "Operate the Program", "Run the controls and preserve dated evidence before engaging the CPA firm.", [
|
|
482
516
|
item(
|
|
483
517
|
"candidate-type-one-date",
|
|
@@ -492,9 +526,13 @@ function operationStage(workspace, scope, records, byId, asOf, evidenceReady) {
|
|
|
492
526
|
]);
|
|
493
527
|
}
|
|
494
528
|
|
|
495
|
-
const obligations = planObligations(records, { asOf, through: asOf });
|
|
496
|
-
const start = workspace.
|
|
497
|
-
|
|
529
|
+
const obligations = planObligations(records, { asOf, through: asOf, model });
|
|
530
|
+
const start = workspace.candidateCoverage?.kind === "range"
|
|
531
|
+
? coverageStart(workspace.candidateCoverage)
|
|
532
|
+
: null;
|
|
533
|
+
const end = workspace.candidateCoverage?.kind === "range"
|
|
534
|
+
? coverageEnd(workspace.candidateCoverage)
|
|
535
|
+
: null;
|
|
498
536
|
const startStatus = !start ? "action" : start <= asOf ? "complete" : "later";
|
|
499
537
|
return stage("operation", "Operate the Program", "Start the management candidate Type 2 period only after the Evidence Ready gate, then keep collection running.", [
|
|
500
538
|
item(
|
|
@@ -536,7 +574,7 @@ function riskAssessmentItem(scope, records, byId, asOf) {
|
|
|
536
574
|
&& record.status === "complete"
|
|
537
575
|
&& record.methodology
|
|
538
576
|
&& record.approvedOn
|
|
539
|
-
&& record.
|
|
577
|
+
&& record.completedOn >= shiftYear(asOf, -1)
|
|
540
578
|
&& partiesIndependent(record.assessorIds, record.reviewerIds, byId)
|
|
541
579
|
&& (!scope.systems.length || !(record.systemIds || []).length || record.systemIds.some((id) => scope.systems.some((system) => system.id === id)))
|
|
542
580
|
));
|
|
@@ -562,9 +600,10 @@ export function selectedControlFamilies(controls, model) {
|
|
|
562
600
|
id: definition.id,
|
|
563
601
|
title: definition.title,
|
|
564
602
|
sourceKinds: definition.sourceKinds || [],
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
603
|
+
evidenceForm: definition.evidenceForm || "capture",
|
|
604
|
+
evidencePrompt: definition.evidencePrompt || `Retain usable evidence for ${definition.title.toLowerCase()}.`,
|
|
605
|
+
description: definition.description || "",
|
|
606
|
+
timing: definition.timing || "",
|
|
568
607
|
operationRecordTypes: definition.operationRecordTypes || [],
|
|
569
608
|
controls: selected
|
|
570
609
|
});
|
|
@@ -586,9 +625,10 @@ export function selectedControlFamilies(controls, model) {
|
|
|
586
625
|
id: `control-${prefix}`,
|
|
587
626
|
title,
|
|
588
627
|
sourceKinds: [],
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
628
|
+
evidenceForm: "capture",
|
|
629
|
+
evidencePrompt: `Retain usable evidence for the selected ${title.toLowerCase()} controls.`,
|
|
630
|
+
description: `Record the authoritative Systems that produce evidence for the selected ${title.toLowerCase()} controls.`,
|
|
631
|
+
timing: "Map the source and document repeatable retrieval before program operation begins.",
|
|
592
632
|
operationRecordTypes: [],
|
|
593
633
|
controls: selected
|
|
594
634
|
});
|
|
@@ -596,10 +636,6 @@ export function selectedControlFamilies(controls, model) {
|
|
|
596
636
|
return families;
|
|
597
637
|
}
|
|
598
638
|
|
|
599
|
-
function requiresCollectionTest(family) {
|
|
600
|
-
return family.collectionTestRequired !== false;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
639
|
async function primaryMarkdown(loaded, record) {
|
|
604
640
|
const definition = loaded.model.resources[record.type];
|
|
605
641
|
const selected = markdownEntries(loaded.model, record).find((entry) => (
|
|
@@ -657,7 +693,7 @@ function controlCheckLabel(name) {
|
|
|
657
693
|
owner: "owner",
|
|
658
694
|
procedure: "actual procedure in Record Markdown",
|
|
659
695
|
scope: "in-scope systems",
|
|
660
|
-
|
|
696
|
+
operationPattern: "operation pattern",
|
|
661
697
|
evidenceSource: "authoritative evidence source",
|
|
662
698
|
implementationDate: "implementation date",
|
|
663
699
|
policyMapping: "policy mapping",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function effectiveResourceStatus(record, asOf) {
|
|
2
|
+
if (
|
|
3
|
+
record?.type === "attestation"
|
|
4
|
+
&& record.status === "pending"
|
|
5
|
+
&& record.dueOn
|
|
6
|
+
&& asOf
|
|
7
|
+
&& record.dueOn < asOf
|
|
8
|
+
) return "overdue";
|
|
9
|
+
if (
|
|
10
|
+
record?.type === "evidence"
|
|
11
|
+
&& ["collected", "verified"].includes(record.status)
|
|
12
|
+
&& record.expiresOn
|
|
13
|
+
&& asOf
|
|
14
|
+
&& record.expiresOn < asOf
|
|
15
|
+
) return "expired";
|
|
16
|
+
return record?.status;
|
|
17
|
+
}
|