filegrc 0.1.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/LICENSE +21 -0
- package/README.md +42 -0
- package/bin/filegrc.js +8 -0
- package/model/index.js +13 -0
- package/model/v1.json +1378 -0
- package/package.json +29 -0
- package/src/agent.js +247 -0
- package/src/audit-preparation.js +906 -0
- package/src/build.js +27 -0
- package/src/cli.js +623 -0
- package/src/evidence-packet.js +1642 -0
- package/src/favicon.js +109 -0
- package/src/files.js +533 -0
- package/src/git.js +289 -0
- package/src/id.js +19 -0
- package/src/index.js +47 -0
- package/src/markdown.js +123 -0
- package/src/model-docs.js +119 -0
- package/src/mutation.js +15 -0
- package/src/obligations.js +595 -0
- package/src/paths.js +137 -0
- package/src/recurrence.js +89 -0
- package/src/resource-markdown.js +63 -0
- package/src/search.js +27 -0
- package/src/server.js +300 -0
- package/src/state.js +84 -0
- package/src/time.js +62 -0
- package/src/validate.js +496 -0
- package/src/web.js +2328 -0
- package/src/workspace.js +90 -0
|
@@ -0,0 +1,906 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { createResource, createResources, deleteResource, updateResource } from "./files.js";
|
|
4
|
+
import { createResourceId } from "./id.js";
|
|
5
|
+
import { resolveDataPath } from "./paths.js";
|
|
6
|
+
import { markdownEntries } from "./resource-markdown.js";
|
|
7
|
+
import { loadWorkspace } from "./workspace.js";
|
|
8
|
+
|
|
9
|
+
export async function assessAuditPreparation(input, options = {}) {
|
|
10
|
+
const loaded = input?.resources && input?.model && input?.entries
|
|
11
|
+
? input
|
|
12
|
+
: await loadWorkspace(input);
|
|
13
|
+
const records = loaded.resources;
|
|
14
|
+
const byId = new Map(records.map((record) => [record.id, record]));
|
|
15
|
+
const audits = records.filter((record) => record.type === "audit");
|
|
16
|
+
const audit = options.auditId
|
|
17
|
+
? audits.find((record) => record.id === options.auditId)
|
|
18
|
+
: options.selectDefault === false
|
|
19
|
+
? null
|
|
20
|
+
: audits.find((record) => !["complete", "closed", "canceled"].includes(record.status)) || audits[0];
|
|
21
|
+
if (options.auditId && !audit) throw new Error(`Audit "${options.auditId}" was not found.`);
|
|
22
|
+
|
|
23
|
+
const stages = [];
|
|
24
|
+
stages.push(scopeStage(audit, records, byId));
|
|
25
|
+
stages.push(programStage(audit, records, byId));
|
|
26
|
+
stages.push(await documentsStage(loaded, audit, byId));
|
|
27
|
+
stages.push(evidenceStage(audit, records, byId, loaded.model));
|
|
28
|
+
stages.push(populationsStage(audit, records, byId, loaded.model));
|
|
29
|
+
stages.push(auditorStage(loaded.model));
|
|
30
|
+
|
|
31
|
+
for (const stage of stages) {
|
|
32
|
+
stage.counts = countStatuses(stage.items);
|
|
33
|
+
stage.status = stage.counts.action ? "action" : stage.counts.later ? "later" : "complete";
|
|
34
|
+
}
|
|
35
|
+
const items = stages.flatMap((stage) => stage.items);
|
|
36
|
+
const counts = countStatuses(items);
|
|
37
|
+
const managedItems = items.filter((item) => !["external", "info", "later"].includes(item.status));
|
|
38
|
+
const completedManagedItems = managedItems.filter((item) => item.status === "complete");
|
|
39
|
+
return {
|
|
40
|
+
schemaVersion: 1,
|
|
41
|
+
generatedAt: options.generatedAt || new Date().toISOString(),
|
|
42
|
+
audit: audit ? auditSummary(audit) : null,
|
|
43
|
+
status: counts.action ? "needs-work" : audit ? "management-ready" : "not-started",
|
|
44
|
+
progress: {
|
|
45
|
+
complete: completedManagedItems.length,
|
|
46
|
+
total: managedItems.length,
|
|
47
|
+
percent: managedItems.length ? Math.round((completedManagedItems.length / managedItems.length) * 100) : 0
|
|
48
|
+
},
|
|
49
|
+
counts,
|
|
50
|
+
canInitialize: Boolean(audit
|
|
51
|
+
&& ["soc-2-type-1", "soc-2-type-2"].includes(audit.auditKind)
|
|
52
|
+
&& (audit.auditKind === "soc-2-type-1"
|
|
53
|
+
? audit.typeOneAsOf
|
|
54
|
+
: audit.periodStart && audit.periodEnd)
|
|
55
|
+
&& initializationNeeded(audit, records, loaded.model)),
|
|
56
|
+
stages
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function prepareAuditWorkspace(input, options = {}) {
|
|
61
|
+
const loaded = await loadWorkspace(input);
|
|
62
|
+
const audit = loaded.resources.find((record) => record.type === "audit" && record.id === options.auditId);
|
|
63
|
+
if (!audit) throw new Error(`Audit "${options.auditId || ""}" was not found.`);
|
|
64
|
+
if (!["soc-2-type-1", "soc-2-type-2"].includes(audit.auditKind)) {
|
|
65
|
+
throw new Error("Audit preparation requires a SOC 2 Type 1 or Type 2 engagement.");
|
|
66
|
+
}
|
|
67
|
+
if (audit.auditKind === "soc-2-type-2" && (!audit.periodStart || !audit.periodEnd)) {
|
|
68
|
+
throw new Error("Set the Type 2 audit period before initializing audit preparation.");
|
|
69
|
+
}
|
|
70
|
+
if (audit.auditKind === "soc-2-type-1" && !audit.typeOneAsOf) {
|
|
71
|
+
throw new Error("Set the Type 1 as-of date before initializing audit preparation.");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const model = loaded.model.auditReadiness || {};
|
|
75
|
+
const auditEntry = loaded.entries.find((entry) => entry.record.id === audit.id);
|
|
76
|
+
const auditRevision = createHash("sha256").update(auditEntry.source).digest("hex");
|
|
77
|
+
const documents = loaded.resources.filter((record) => record.type === "document");
|
|
78
|
+
const nextAudit = { ...audit };
|
|
79
|
+
const linkedDocuments = [];
|
|
80
|
+
const createdDocuments = [];
|
|
81
|
+
const existingIds = loaded.resources.map((record) => record.id);
|
|
82
|
+
try {
|
|
83
|
+
for (const definition of applicableManagementDocuments(audit, model)) {
|
|
84
|
+
const linked = nextAudit[definition.field]
|
|
85
|
+
? documents.find((record) => record.id === nextAudit[definition.field])
|
|
86
|
+
: null;
|
|
87
|
+
if (linked && linked.template !== true) continue;
|
|
88
|
+
const template = linked
|
|
89
|
+
|| documents.find((record) => record.documentKind === definition.kind && record.template === true)
|
|
90
|
+
|| documents.find((record) => record.documentKind === definition.kind);
|
|
91
|
+
if (!template) continue;
|
|
92
|
+
const source = await primaryMarkdown(loaded, template);
|
|
93
|
+
if (!source) throw new Error(`The ${definition.title} template has no Markdown content.`);
|
|
94
|
+
const id = createResourceId("document", `${audit.id} ${definition.kind}`, existingIds);
|
|
95
|
+
existingIds.push(id);
|
|
96
|
+
const document = engagementDocument(template, definition, audit, id);
|
|
97
|
+
const content = materializeManagementMarkdown(source, audit, loaded.resources);
|
|
98
|
+
await createResource(loaded.root, document, { content: { content } });
|
|
99
|
+
documents.push(document);
|
|
100
|
+
createdDocuments.push(document);
|
|
101
|
+
nextAudit[definition.field] = id;
|
|
102
|
+
linkedDocuments.push(id);
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
for (const document of createdDocuments.reverse()) {
|
|
106
|
+
await deleteResource(loaded.root, document.type, document.id).catch(() => {});
|
|
107
|
+
}
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const existingKinds = new Set(loaded.resources
|
|
112
|
+
.filter((record) => record.type === "audit-population" && record.auditId === audit.id)
|
|
113
|
+
.map((record) => record.populationKind));
|
|
114
|
+
const selectedControls = (audit.controlIds || [])
|
|
115
|
+
.map((id) => loaded.resources.find((record) => record.id === id))
|
|
116
|
+
.filter(Boolean);
|
|
117
|
+
const sourceSystems = loaded.resources.filter((record) => record.type === "system");
|
|
118
|
+
const populations = (audit.auditKind === "soc-2-type-2" ? model.populationTemplates || [] : [])
|
|
119
|
+
.filter((template) => !existingKinds.has(template.kind))
|
|
120
|
+
.map((template) => {
|
|
121
|
+
const id = createResourceId(
|
|
122
|
+
"audit-population",
|
|
123
|
+
`${template.kind} ${audit.id}`,
|
|
124
|
+
existingIds
|
|
125
|
+
);
|
|
126
|
+
existingIds.push(id);
|
|
127
|
+
const controlIds = selectedControls
|
|
128
|
+
.filter((control) => (template.controlCodes || []).includes(control.code))
|
|
129
|
+
.map((control) => control.id);
|
|
130
|
+
const matchingSources = sourceSystems.filter((system) => (
|
|
131
|
+
(system.evidenceSourceKinds || []).includes(template.sourceKind)
|
|
132
|
+
));
|
|
133
|
+
return {
|
|
134
|
+
schemaVersion: 1,
|
|
135
|
+
id,
|
|
136
|
+
type: "audit-population",
|
|
137
|
+
title: template.title,
|
|
138
|
+
status: "planned",
|
|
139
|
+
auditId: audit.id,
|
|
140
|
+
populationKind: template.kind,
|
|
141
|
+
periodStart: audit.periodStart,
|
|
142
|
+
periodEnd: audit.periodEnd,
|
|
143
|
+
ownerIds: [...audit.ownerIds],
|
|
144
|
+
...(controlIds.length ? { controlIds } : {}),
|
|
145
|
+
...(matchingSources.length === 1 ? { sourceSystemId: matchingSources[0].id } : {}),
|
|
146
|
+
reconciliationSummary: `Authoritative source to confirm: ${template.sourcePrompt}. ${template.timing || ""}`.trim()
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
if (populations.length) await createResources(loaded.root, populations);
|
|
152
|
+
if (JSON.stringify(nextAudit) !== JSON.stringify(audit)) {
|
|
153
|
+
await updateResource(loaded.root, "audit", audit.id, nextAudit, { expectedRevision: auditRevision });
|
|
154
|
+
}
|
|
155
|
+
} catch (error) {
|
|
156
|
+
for (const population of populations.reverse()) {
|
|
157
|
+
await deleteResource(loaded.root, population.type, population.id).catch(() => {});
|
|
158
|
+
}
|
|
159
|
+
for (const document of createdDocuments.reverse()) {
|
|
160
|
+
await deleteResource(loaded.root, document.type, document.id).catch(() => {});
|
|
161
|
+
}
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
auditId: audit.id,
|
|
166
|
+
linkedDocumentIds: linkedDocuments,
|
|
167
|
+
createdDocumentIds: createdDocuments.map((record) => record.id),
|
|
168
|
+
createdPopulationIds: populations.map((record) => record.id)
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function scopeStage(audit, records, byId) {
|
|
173
|
+
const items = [];
|
|
174
|
+
items.push(item(
|
|
175
|
+
"engagement",
|
|
176
|
+
audit ? "complete" : "action",
|
|
177
|
+
"Create the engagement",
|
|
178
|
+
audit
|
|
179
|
+
? `${audit.title} is the audit record used for scope, dates, the CPA firm, requests, and the final report.`
|
|
180
|
+
: "Create a SOC 2 Type 2 audit record before collecting period evidence.",
|
|
181
|
+
audit || { type: "audit" }
|
|
182
|
+
));
|
|
183
|
+
if (!audit) {
|
|
184
|
+
return stage("scope", "Scope and Engagement", "Define the report, service boundary, criteria, dependencies, and CPA firm.", items);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const periodComplete = audit.auditKind === "soc-2-type-2"
|
|
188
|
+
? audit.periodStart && audit.periodEnd
|
|
189
|
+
: audit.auditKind === "soc-2-type-1"
|
|
190
|
+
? audit.typeOneAsOf
|
|
191
|
+
: false;
|
|
192
|
+
items.push(item(
|
|
193
|
+
"period",
|
|
194
|
+
periodComplete ? "complete" : "action",
|
|
195
|
+
"Set the report type and date",
|
|
196
|
+
periodComplete
|
|
197
|
+
? audit.auditKind === "soc-2-type-2"
|
|
198
|
+
? `Type 2 period: ${audit.periodStart} through ${audit.periodEnd}.`
|
|
199
|
+
: `Type 1 as-of date: ${audit.typeOneAsOf}.`
|
|
200
|
+
: audit.auditKind === "soc-2-type-1"
|
|
201
|
+
? "Set the Type 1 as-of date."
|
|
202
|
+
: audit.auditKind === "soc-2-type-2"
|
|
203
|
+
? "Set the exact Type 2 start and end dates."
|
|
204
|
+
: "Change this readiness record to a Type 1 or Type 2 engagement before planning the report.",
|
|
205
|
+
audit
|
|
206
|
+
));
|
|
207
|
+
|
|
208
|
+
const systems = (audit.systemIds || []).map((id) => byId.get(id)).filter(Boolean);
|
|
209
|
+
const completeSystems = systems.filter((system) => (
|
|
210
|
+
system.status === "active"
|
|
211
|
+
&& system.inScope === true
|
|
212
|
+
&& system.description
|
|
213
|
+
&& system.dataClassification
|
|
214
|
+
&& (system.ownerIds || []).length
|
|
215
|
+
));
|
|
216
|
+
items.push(item(
|
|
217
|
+
"systems",
|
|
218
|
+
systems.length && completeSystems.length === systems.length ? "complete" : "action",
|
|
219
|
+
"Define the service boundary",
|
|
220
|
+
systems.length
|
|
221
|
+
? `${completeSystems.length} of ${systems.length} selected systems are active, explicitly in scope, owned, classified, and described.`
|
|
222
|
+
: "Select every in-scope service and supporting system, then describe its owner, environment, data, vendors, and boundary.",
|
|
223
|
+
systems[0] || { type: "system" }
|
|
224
|
+
));
|
|
225
|
+
|
|
226
|
+
const engagementStart = audit.auditKind === "soc-2-type-1" ? audit.typeOneAsOf : audit.periodStart;
|
|
227
|
+
const commitments = records.filter((record) => record.type === "commitment"
|
|
228
|
+
&& record.status === "active"
|
|
229
|
+
&& systems.some((system) => (
|
|
230
|
+
(system.commitmentIds || []).includes(record.id) || (record.systemIds || []).includes(system.id)
|
|
231
|
+
)));
|
|
232
|
+
const completeCommitments = commitments.filter((commitment) => (
|
|
233
|
+
commitment.statement
|
|
234
|
+
&& (commitment.ownerIds || []).length
|
|
235
|
+
&& commitment.effectiveOn
|
|
236
|
+
&& (!engagementStart || commitment.effectiveOn <= engagementStart)
|
|
237
|
+
&& (commitment.requirementIds || []).length
|
|
238
|
+
&& (commitment.controlIds || []).length
|
|
239
|
+
));
|
|
240
|
+
const systemsWithoutCommitments = systems.filter((system) => !completeCommitments.some((commitment) => (
|
|
241
|
+
(system.commitmentIds || []).includes(commitment.id) || (commitment.systemIds || []).includes(system.id)
|
|
242
|
+
)));
|
|
243
|
+
items.push(item(
|
|
244
|
+
"commitments",
|
|
245
|
+
systems.length && !systemsWithoutCommitments.length ? "complete" : "action",
|
|
246
|
+
"Record commitments and system requirements",
|
|
247
|
+
systems.length
|
|
248
|
+
? `${completeCommitments.length} complete active commitments cover ${systems.length - systemsWithoutCommitments.length} of ${systems.length} in-scope systems.`
|
|
249
|
+
: "Record customer commitments and internal system requirements after defining the service boundary.",
|
|
250
|
+
commitments[0] || { type: "commitment" }
|
|
251
|
+
));
|
|
252
|
+
|
|
253
|
+
const frameworkRequirementIds = records
|
|
254
|
+
.filter((record) => record.type === "requirement" && (audit.frameworkIds || []).includes(record.frameworkId))
|
|
255
|
+
.map((record) => record.id);
|
|
256
|
+
const unresolvedRequirements = frameworkRequirementIds
|
|
257
|
+
.map((id) => byId.get(id))
|
|
258
|
+
.filter((requirement) => (
|
|
259
|
+
requirement.applicability === "undetermined"
|
|
260
|
+
|| (requirement.applicability === "not-applicable" && !requirement.applicabilityRationale)
|
|
261
|
+
));
|
|
262
|
+
const applicableRequirementIds = frameworkRequirementIds.filter((id) => byId.get(id)?.applicability === "applicable");
|
|
263
|
+
const missingApplicableRequirements = applicableRequirementIds.filter((id) => !(audit.requirementIds || []).includes(id));
|
|
264
|
+
const selectedRequirements = (audit.requirementIds || []).map((id) => byId.get(id)).filter(Boolean);
|
|
265
|
+
const unexpectedRequirements = selectedRequirements.filter((requirement) => (
|
|
266
|
+
!(audit.frameworkIds || []).includes(requirement.frameworkId)
|
|
267
|
+
|| requirement.applicability !== "applicable"
|
|
268
|
+
));
|
|
269
|
+
const descriptionCriteriaSelected = selectedRequirements.some((requirement) => (
|
|
270
|
+
(requirement.tags || []).includes("description-criteria")
|
|
271
|
+
|| /^DC\d+/i.test(requirement.reference || "")
|
|
272
|
+
));
|
|
273
|
+
const criteriaComplete = (audit.frameworkIds || []).length
|
|
274
|
+
&& (audit.requirementIds || []).length
|
|
275
|
+
&& (audit.controlIds || []).length
|
|
276
|
+
&& !unresolvedRequirements.length
|
|
277
|
+
&& !missingApplicableRequirements.length
|
|
278
|
+
&& !unexpectedRequirements.length
|
|
279
|
+
&& descriptionCriteriaSelected;
|
|
280
|
+
items.push(item(
|
|
281
|
+
"criteria",
|
|
282
|
+
criteriaComplete ? "complete" : "action",
|
|
283
|
+
"Confirm criteria and controls in scope",
|
|
284
|
+
criteriaComplete
|
|
285
|
+
? `${audit.requirementIds.length} applicable criteria and ${audit.controlIds.length} controls are selected, with applicability resolved for the selected frameworks.`
|
|
286
|
+
: unresolvedRequirements.length
|
|
287
|
+
? `Resolve applicability and record a rationale for ${unresolvedRequirements.length} selected-framework criteria.`
|
|
288
|
+
: missingApplicableRequirements.length
|
|
289
|
+
? `Add ${missingApplicableRequirements.length} applicable selected-framework criteria to the engagement.`
|
|
290
|
+
: unexpectedRequirements.length
|
|
291
|
+
? `Remove ${unexpectedRequirements.length} criteria that are not applicable members of the selected frameworks.`
|
|
292
|
+
: !descriptionCriteriaSelected
|
|
293
|
+
? "Select the applicable SOC 2 description criteria as well as the Trust Services Criteria."
|
|
294
|
+
: "Select the Security criteria, any optional Trust Services Categories, and the controls included in this report.",
|
|
295
|
+
audit
|
|
296
|
+
));
|
|
297
|
+
|
|
298
|
+
const expectedSubserviceVendorIds = new Set(systems.flatMap((system) => system.subserviceVendorIds || []));
|
|
299
|
+
const missingSubserviceVendorIds = [...expectedSubserviceVendorIds].filter((id) => !(audit.subserviceVendorIds || []).includes(id));
|
|
300
|
+
const inclusiveSystemIds = records
|
|
301
|
+
.filter((record) => record.type === "system" && (audit.subserviceVendorIds || []).includes(record.vendorId))
|
|
302
|
+
.map((record) => record.id);
|
|
303
|
+
const inclusiveControlCount = (audit.controlIds || [])
|
|
304
|
+
.map((id) => byId.get(id))
|
|
305
|
+
.filter((control) => (control?.systemIds || []).some((id) => inclusiveSystemIds.includes(id)))
|
|
306
|
+
.length;
|
|
307
|
+
const subserviceComplete = Boolean(audit.subserviceMethod)
|
|
308
|
+
&& !((audit.subserviceVendorIds || []).length && audit.subserviceMethod === "not-applicable")
|
|
309
|
+
&& !missingSubserviceVendorIds.length
|
|
310
|
+
&& !(audit.subserviceMethod === "inclusive" && (!inclusiveSystemIds.length || !inclusiveControlCount));
|
|
311
|
+
items.push(item(
|
|
312
|
+
"subservices",
|
|
313
|
+
subserviceComplete ? "complete" : "action",
|
|
314
|
+
"Decide how subservice organizations are presented",
|
|
315
|
+
subserviceComplete
|
|
316
|
+
? `${displayValue(audit.subserviceMethod)} method selected for ${(audit.subserviceVendorIds || []).length} subservice organizations${audit.subserviceMethod === "inclusive" ? `, with ${inclusiveControlCount} included controls` : ""}.`
|
|
317
|
+
: missingSubserviceVendorIds.length
|
|
318
|
+
? `Add ${missingSubserviceVendorIds.length} subservice organizations already identified by the in-scope systems.`
|
|
319
|
+
: audit.subserviceMethod === "inclusive"
|
|
320
|
+
? "For the inclusive method, catalog the subservice systems and include the subservice controls the auditor will examine."
|
|
321
|
+
: "Identify relevant infrastructure and service providers, then agree on carve-out, inclusive, or not-applicable treatment.",
|
|
322
|
+
audit
|
|
323
|
+
));
|
|
324
|
+
|
|
325
|
+
const relevantComplementaryControls = records.filter((record) => (
|
|
326
|
+
record.type === "complementary-control"
|
|
327
|
+
&& record.status === "active"
|
|
328
|
+
&& (record.systemIds || []).some((id) => (audit.systemIds || []).includes(id))
|
|
329
|
+
));
|
|
330
|
+
const selectedComplementaryControls = (audit.complementaryControlIds || []).map((id) => byId.get(id)).filter(Boolean);
|
|
331
|
+
const complementaryComplete = audit.complementaryControlsConclusion === "not-applicable"
|
|
332
|
+
? !relevantComplementaryControls.length
|
|
333
|
+
: audit.complementaryControlsConclusion === "identified"
|
|
334
|
+
&& selectedComplementaryControls.length
|
|
335
|
+
&& selectedComplementaryControls.every((control) => (
|
|
336
|
+
control.status === "active"
|
|
337
|
+
&& (control.systemIds || []).some((id) => (audit.systemIds || []).includes(id))
|
|
338
|
+
));
|
|
339
|
+
items.push(item(
|
|
340
|
+
"complementary-controls",
|
|
341
|
+
complementaryComplete ? "complete" : "action",
|
|
342
|
+
"Resolve customer and subservice dependencies",
|
|
343
|
+
complementaryComplete
|
|
344
|
+
? audit.complementaryControlsConclusion === "identified"
|
|
345
|
+
? `${audit.complementaryControlIds.length} complementary controls are selected.`
|
|
346
|
+
: "Management recorded that no complementary controls are needed for the described service."
|
|
347
|
+
: audit.complementaryControlsConclusion === "not-applicable" && relevantComplementaryControls.length
|
|
348
|
+
? `${relevantComplementaryControls.length} active complementary controls apply to the in-scope systems, which conflicts with the not-applicable conclusion.`
|
|
349
|
+
: "State whether customers or subservice organizations must operate complementary controls. If they do, record and select each one.",
|
|
350
|
+
audit
|
|
351
|
+
));
|
|
352
|
+
|
|
353
|
+
const auditor = audit.auditorVendorId ? byId.get(audit.auditorVendorId) : null;
|
|
354
|
+
const auditorNamed = Boolean(auditor || hasMeaningfulValue(audit.auditor));
|
|
355
|
+
items.push(item(
|
|
356
|
+
"auditor",
|
|
357
|
+
auditorNamed ? "complete" : "action",
|
|
358
|
+
"Engage an independent CPA firm",
|
|
359
|
+
auditorNamed
|
|
360
|
+
? `${auditor?.title || "An independent CPA firm"} is recorded for the engagement.`
|
|
361
|
+
: "Select a CPA firm and agree on scope, timing, subservice treatment, and evidence expectations early.",
|
|
362
|
+
audit
|
|
363
|
+
));
|
|
364
|
+
return stage("scope", "Scope and Engagement", "Define the report, service boundary, criteria, dependencies, and CPA firm.", items);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function programStage(audit, records, byId) {
|
|
368
|
+
const selectedControls = audit?.controlIds?.length
|
|
369
|
+
? audit.controlIds.map((id) => byId.get(id)).filter(Boolean)
|
|
370
|
+
: records.filter((record) => record.type === "control" && record.status !== "retired");
|
|
371
|
+
const policyIds = new Set(selectedControls.flatMap((control) => control.policyIds || []));
|
|
372
|
+
const policies = (policyIds.size
|
|
373
|
+
? [...policyIds].map((id) => byId.get(id)).filter(Boolean)
|
|
374
|
+
: records.filter((record) => record.type === "policy" && !["superseded", "retired"].includes(record.status)));
|
|
375
|
+
const approvedPolicies = policies.filter((policy) => (
|
|
376
|
+
["approved", "active"].includes(policy.status)
|
|
377
|
+
&& policy.approvedOn
|
|
378
|
+
&& policy.effectiveOn
|
|
379
|
+
&& (policy.ownerIds || []).length
|
|
380
|
+
&& (policy.approverIds || []).length
|
|
381
|
+
&& partiesIndependent(policy.ownerIds, policy.approverIds, byId)
|
|
382
|
+
));
|
|
383
|
+
const implementedControls = selectedControls.filter((control) => (
|
|
384
|
+
control.status === "implemented"
|
|
385
|
+
&& control.effectiveOn
|
|
386
|
+
&& control.activity
|
|
387
|
+
&& control.operationMode
|
|
388
|
+
&& control.frequency
|
|
389
|
+
&& (control.ownerIds || []).length
|
|
390
|
+
&& (control.requirementIds || []).length
|
|
391
|
+
&& (control.policyIds || []).length
|
|
392
|
+
&& (!audit?.systemIds?.length || (control.systemIds || []).some((id) => audit.systemIds.includes(id)))
|
|
393
|
+
));
|
|
394
|
+
const assessment = records.find((record) => (
|
|
395
|
+
record.type === "risk-assessment"
|
|
396
|
+
&& record.status === "complete"
|
|
397
|
+
&& record.methodology
|
|
398
|
+
&& record.approvedOn
|
|
399
|
+
&& partiesIndependent(record.assessorIds, record.reviewerIds, byId)
|
|
400
|
+
&& (!audit?.periodEnd || (record.assessmentDate <= audit.periodEnd && record.assessmentDate >= shiftYear(audit.periodEnd, -1)))
|
|
401
|
+
&& (!audit?.systemIds?.length || !(record.systemIds || []).length || record.systemIds.some((id) => audit.systemIds.includes(id)))
|
|
402
|
+
));
|
|
403
|
+
return stage("program", "Adopt and Implement", "Approve the rules, confirm the controls match actual operation, and assess risk.", [
|
|
404
|
+
item(
|
|
405
|
+
"policies",
|
|
406
|
+
policies.length && approvedPolicies.length === policies.length ? "complete" : "action",
|
|
407
|
+
"Approve the applicable policies",
|
|
408
|
+
policies.length
|
|
409
|
+
? `${approvedPolicies.length} of ${policies.length} applicable policies have an owner, separate approver, approval date, and effective date.`
|
|
410
|
+
: "Link the controls to the policies management will adopt.",
|
|
411
|
+
approvedPolicies[0] || policies[0] || { type: "policy" }
|
|
412
|
+
),
|
|
413
|
+
item(
|
|
414
|
+
"controls",
|
|
415
|
+
selectedControls.length && implementedControls.length === selectedControls.length ? "complete" : "action",
|
|
416
|
+
"Confirm every control is implemented",
|
|
417
|
+
selectedControls.length
|
|
418
|
+
? `${implementedControls.length} of ${selectedControls.length} selected controls are implemented, effective, owned, mapped, scoped, and define their activity, mode, and frequency.`
|
|
419
|
+
: "Select the controls for the engagement, then confirm each statement matches current practice.",
|
|
420
|
+
implementedControls[0] || selectedControls[0] || { type: "control" }
|
|
421
|
+
),
|
|
422
|
+
item(
|
|
423
|
+
"risk-assessment",
|
|
424
|
+
assessment ? "complete" : "action",
|
|
425
|
+
"Complete the in-scope risk assessment",
|
|
426
|
+
assessment
|
|
427
|
+
? `${assessment.title} is the most recent completed, approved, and independently reviewed assessment found for the service.`
|
|
428
|
+
: "Complete an in-scope risk assessment during the year ending with the report date or period, using the approved method and a reviewer separate from the assessor.",
|
|
429
|
+
assessment || { type: "risk-assessment" }
|
|
430
|
+
)
|
|
431
|
+
]);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function documentsStage(loaded, audit, byId) {
|
|
435
|
+
const definitions = applicableManagementDocuments(audit, loaded.model.auditReadiness || {});
|
|
436
|
+
const items = [];
|
|
437
|
+
for (const definition of definitions) {
|
|
438
|
+
const document = audit?.[definition.field] ? byId.get(audit[definition.field]) : null;
|
|
439
|
+
const source = document ? await primaryMarkdown(loaded, document) : "";
|
|
440
|
+
const contentIssues = managementDocumentContentIssues(source, definition, audit);
|
|
441
|
+
const engagementEnd = audit?.auditKind === "soc-2-type-1" ? audit.typeOneAsOf : audit?.periodEnd;
|
|
442
|
+
if (document?.approvedOn && engagementEnd && document.approvedOn < engagementEnd) {
|
|
443
|
+
contentIssues.push(`Approve the final document on or after the engagement ${audit.auditKind === "soc-2-type-1" ? "date" : "period end"}.`);
|
|
444
|
+
}
|
|
445
|
+
if (definition.kind === "soc2-period-completeness" && document?.approvedOn && audit) {
|
|
446
|
+
const latestReconciliation = loaded.resources
|
|
447
|
+
.filter((record) => record.type === "audit-population" && record.auditId === audit.id)
|
|
448
|
+
.map((record) => record.reconciledOn)
|
|
449
|
+
.filter(Boolean)
|
|
450
|
+
.sort()
|
|
451
|
+
.at(-1);
|
|
452
|
+
if (latestReconciliation && document.approvedOn < latestReconciliation) {
|
|
453
|
+
contentIssues.push("Approve the period completeness statement after the last population reconciliation.");
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (definition.kind === "soc2-management-representation" && document) {
|
|
457
|
+
const signedEvidence = (document.evidenceIds || [])
|
|
458
|
+
.map((id) => byId.get(id))
|
|
459
|
+
.find((record) => (
|
|
460
|
+
record?.type === "evidence"
|
|
461
|
+
&& record.status === "verified"
|
|
462
|
+
&& (record.filePaths || []).length
|
|
463
|
+
));
|
|
464
|
+
if (!signedEvidence) contentIssues.push("Link a verified fixed-format copy of the signed representation letter as evidence.");
|
|
465
|
+
else if (!signedEvidence.collectedOn || (engagementEnd && signedEvidence.collectedOn < engagementEnd)) {
|
|
466
|
+
contentIssues.push(`The signed representation must be dated on or after the engagement ${audit.auditKind === "soc-2-type-1" ? "date" : "period end"}.`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const complete = Boolean(
|
|
470
|
+
document
|
|
471
|
+
&& document.type === "document"
|
|
472
|
+
&& document.template !== true
|
|
473
|
+
&& document.status === "active"
|
|
474
|
+
&& document.approvedOn
|
|
475
|
+
&& document.effectiveOn
|
|
476
|
+
&& (document.ownerIds || []).length
|
|
477
|
+
&& (document.approverIds || []).length
|
|
478
|
+
&& partiesIndependent(document.ownerIds, document.approverIds, byId)
|
|
479
|
+
&& source
|
|
480
|
+
&& !contentIssues.length
|
|
481
|
+
);
|
|
482
|
+
const representationLater = definition.kind === "soc2-management-representation"
|
|
483
|
+
&& audit
|
|
484
|
+
&& !["fieldwork", "complete"].includes(audit.status);
|
|
485
|
+
items.push(item(
|
|
486
|
+
definition.kind,
|
|
487
|
+
complete ? "complete" : representationLater ? "later" : "action",
|
|
488
|
+
definition.title,
|
|
489
|
+
complete
|
|
490
|
+
? "Linked Markdown is complete, active, approved, and effective."
|
|
491
|
+
: document
|
|
492
|
+
? `${definition.timing} ${contentIssues[0] || "Complete and approve the engagement-specific document."}`
|
|
493
|
+
: `Link the starter ${definition.title.toLowerCase()} to this audit. ${definition.timing}`,
|
|
494
|
+
document || { type: "document" }
|
|
495
|
+
));
|
|
496
|
+
}
|
|
497
|
+
return stage("documents", "Management Documents", "Prepare management's description, assertions, completeness work, and closing representations.", items);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function evidenceStage(audit, records, byId, model) {
|
|
501
|
+
const controls = (audit?.controlIds || []).map((id) => byId.get(id)).filter(Boolean);
|
|
502
|
+
const evidence = records.filter((record) => record.type === "evidence");
|
|
503
|
+
const periodEvidence = evidence.filter((record) => (
|
|
504
|
+
record.status === "verified"
|
|
505
|
+
&& (record.evidenceKind !== "rendered-record" || record.sourceCommit)
|
|
506
|
+
&& evidenceRelevantToAuditDate(record, audit)
|
|
507
|
+
));
|
|
508
|
+
const controlsWithEvidence = controls.filter((control) => periodEvidence.some((record) => (
|
|
509
|
+
controlIdsForRecord(record, byId).has(control.id)
|
|
510
|
+
)));
|
|
511
|
+
const items = [
|
|
512
|
+
item(
|
|
513
|
+
"control-evidence",
|
|
514
|
+
controls.length && controlsWithEvidence.length === controls.length ? "complete" : "action",
|
|
515
|
+
"Collect operating evidence from authoritative systems",
|
|
516
|
+
controls.length
|
|
517
|
+
? `${controlsWithEvidence.length} of ${controls.length} selected controls have verified evidence for the period.`
|
|
518
|
+
: "Select the engagement controls before checking their operating evidence.",
|
|
519
|
+
periodEvidence[0] || { type: "evidence" }
|
|
520
|
+
)
|
|
521
|
+
];
|
|
522
|
+
const systems = records.filter((record) => record.type === "system" && record.status === "active");
|
|
523
|
+
for (const source of model.auditReadiness?.externalEvidence || []) {
|
|
524
|
+
const relevantControls = controls.filter((control) => (source.controlCodes || []).includes(control.code));
|
|
525
|
+
if (!relevantControls.length) {
|
|
526
|
+
items.push(item(
|
|
527
|
+
`source-${source.id}`,
|
|
528
|
+
"info",
|
|
529
|
+
source.title,
|
|
530
|
+
`No mapped controls from this source category are selected. ${source.timing}`
|
|
531
|
+
));
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
const sourceSystems = systems.filter((system) => (
|
|
535
|
+
(system.evidenceSourceKinds || []).some((kind) => (source.sourceKinds || []).includes(kind))
|
|
536
|
+
));
|
|
537
|
+
const coveredControls = relevantControls.filter((control) => periodEvidence.some((record) => (
|
|
538
|
+
sourceSystems.some((system) => system.id === record.sourceSystemId)
|
|
539
|
+
&& controlIdsForRecord(record, byId).has(control.id)
|
|
540
|
+
)));
|
|
541
|
+
const status = sourceSystems.length && coveredControls.length === relevantControls.length ? "complete" : "action";
|
|
542
|
+
const message = !sourceSystems.length
|
|
543
|
+
? `${source.description} Add the authoritative systems to Systems and assign these evidence source roles: ${(source.sourceKinds || []).map(displayValue).join(", ")}. ${source.timing}`
|
|
544
|
+
: coveredControls.length !== relevantControls.length
|
|
545
|
+
? `${sourceSystems.map((system) => system.title).join(", ")} ${sourceSystems.length === 1 ? "is" : "are"} cataloged, but verified source evidence covers ${coveredControls.length} of ${relevantControls.length} mapped controls. ${source.timing}`
|
|
546
|
+
: `${sourceSystems.map((system) => system.title).join(", ")} provide verified source evidence for ${relevantControls.length} mapped controls. ${source.timing}`;
|
|
547
|
+
items.push(item(
|
|
548
|
+
`source-${source.id}`,
|
|
549
|
+
status,
|
|
550
|
+
source.title,
|
|
551
|
+
message,
|
|
552
|
+
periodEvidence.find((record) => sourceSystems.some((system) => system.id === record.sourceSystemId))
|
|
553
|
+
|| sourceSystems[0]
|
|
554
|
+
|| { type: "system" }
|
|
555
|
+
));
|
|
556
|
+
}
|
|
557
|
+
return stage("evidence", "Operating Evidence Sources", "Catalog each authoritative system, record when to extract its evidence, and bind verified exports to the controls. FileGRC packages the proof but does not replace those systems.", items);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function populationsStage(audit, records, byId, model) {
|
|
561
|
+
if (audit && audit.auditKind !== "soc-2-type-2") {
|
|
562
|
+
return stage(
|
|
563
|
+
"populations",
|
|
564
|
+
"Population Completeness",
|
|
565
|
+
"Complete period populations apply to Type 2 operating-effectiveness testing.",
|
|
566
|
+
[item("type-2-only", "info", "No Type 2 population plan required", "A Type 1 report evaluates design and implementation as of one date. The auditor may still request specific inventories or evidence.")]
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
const populations = audit
|
|
570
|
+
? records.filter((record) => record.type === "audit-population" && record.auditId === audit.id)
|
|
571
|
+
: [];
|
|
572
|
+
const templates = model.auditReadiness?.populationTemplates || [];
|
|
573
|
+
const items = templates.map((template) => {
|
|
574
|
+
const population = populations.find((record) => record.populationKind === template.kind);
|
|
575
|
+
const result = populationResult(population, audit, byId);
|
|
576
|
+
return item(
|
|
577
|
+
`population-${template.kind}`,
|
|
578
|
+
result.status,
|
|
579
|
+
template.title,
|
|
580
|
+
result.message || `Use ${template.sourcePrompt} as the starting point, then record the exact authoritative source.`,
|
|
581
|
+
population || { type: "audit-population" }
|
|
582
|
+
);
|
|
583
|
+
});
|
|
584
|
+
return stage(
|
|
585
|
+
"populations",
|
|
586
|
+
"Population Completeness",
|
|
587
|
+
"Management reconciles complete populations for the exact period. A zero count still needs a source export and recorded query.",
|
|
588
|
+
items
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function auditorStage(model) {
|
|
593
|
+
return stage("auditor", "Auditor-Owned Work", "FileGRC prepares the record set but does not make the CPA firm's independent judgments.", [
|
|
594
|
+
item("firm-eligibility", "external", "Firm eligibility and independence", "Confirm directly with the engagement partner that the firm and signing practitioner meet applicable licensing, peer-review, ethics, and independence requirements. Keep the signed engagement terms with the audit record if management needs a copy."),
|
|
595
|
+
item("sampling", "external", "Sample selection and independent testing", "The auditor chooses samples, performs tests, evaluates exceptions, and decides whether more work is needed."),
|
|
596
|
+
item("report", "external", "Report and opinion", "Management reviews and signs its representations. The auditor issues the final report and opinion."),
|
|
597
|
+
item("criteria", "external", "Authoritative criteria and examination guidance", "FileGRC stores reference IDs and orientation text. Use the publisher's current official criteria and the engagement team's examination guidance for scope, evaluation, and reporting.")
|
|
598
|
+
]);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function populationResult(population, audit, byId) {
|
|
602
|
+
if (!population) return { status: "action", message: "Initialize this population for the engagement." };
|
|
603
|
+
if (population.periodStart !== audit?.periodStart || population.periodEnd !== audit?.periodEnd) {
|
|
604
|
+
return { status: "action", message: "The population period does not match the exact audit period." };
|
|
605
|
+
}
|
|
606
|
+
if (population.status === "not-applicable") {
|
|
607
|
+
if ((population.controlIds || []).length) {
|
|
608
|
+
return { status: "action", message: "This population is linked to in-scope controls, so it cannot be marked not applicable. Reconcile it or remove the incorrect control links." };
|
|
609
|
+
}
|
|
610
|
+
return population.notApplicableReason
|
|
611
|
+
? { status: "complete", message: `Not applicable: ${population.notApplicableReason}` }
|
|
612
|
+
: { status: "action", message: "Document why this population does not apply." };
|
|
613
|
+
}
|
|
614
|
+
if (population.status !== "reconciled") {
|
|
615
|
+
return { status: "action", message: `${displayValue(population.status)}. Export and reconcile the complete population, even when its count is zero.` };
|
|
616
|
+
}
|
|
617
|
+
const evidence = byId.get(population.sourceEvidenceId);
|
|
618
|
+
const requiredEvidence = [
|
|
619
|
+
"generatedAt",
|
|
620
|
+
"timezone",
|
|
621
|
+
"queryDescription",
|
|
622
|
+
"populationCount",
|
|
623
|
+
"completenessValidation",
|
|
624
|
+
"accuracyValidation"
|
|
625
|
+
];
|
|
626
|
+
const evidenceComplete = evidence
|
|
627
|
+
&& evidence.type === "evidence"
|
|
628
|
+
&& evidence.evidenceKind === "population-export"
|
|
629
|
+
&& evidence.status === "verified"
|
|
630
|
+
&& population.sourceSystemId
|
|
631
|
+
&& evidence.sourceSystemId === population.sourceSystemId
|
|
632
|
+
&& evidence.periodStart === audit.periodStart
|
|
633
|
+
&& evidence.periodEnd === audit.periodEnd
|
|
634
|
+
&& requiredEvidence.every((field) => evidence[field] !== undefined && evidence[field] !== null && evidence[field] !== "");
|
|
635
|
+
const reconciliationComplete = (population.reconciledByIds || []).length
|
|
636
|
+
&& population.reconciledOn
|
|
637
|
+
&& ["complete", "complete-with-exceptions"].includes(population.conclusion)
|
|
638
|
+
&& (population.conclusion !== "complete-with-exceptions" || population.reconciliationSummary);
|
|
639
|
+
const generatedOn = timestampDate(evidence?.generatedAt, evidence?.timezone);
|
|
640
|
+
const sequenceComplete = Number.isInteger(evidence?.populationCount)
|
|
641
|
+
&& evidence.populationCount >= 0
|
|
642
|
+
&& generatedOn > audit.periodEnd
|
|
643
|
+
&& population.reconciledOn >= generatedOn;
|
|
644
|
+
if (!evidenceComplete || !reconciliationComplete || !sequenceComplete) {
|
|
645
|
+
return { status: "action", message: "Finish the reconciliation and link a verified population export with its exact query, timezone, count, completeness check, and accuracy check." };
|
|
646
|
+
}
|
|
647
|
+
return {
|
|
648
|
+
status: "complete",
|
|
649
|
+
message: `${evidence.populationCount} items reconciled from ${evidence.source || "the authoritative source"}${population.conclusion === "complete-with-exceptions" ? " with documented exceptions" : ""}.`
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function timestampDate(value, timezone) {
|
|
654
|
+
if (!value || !timezone) return null;
|
|
655
|
+
const instant = new Date(value);
|
|
656
|
+
if (Number.isNaN(instant.getTime())) return null;
|
|
657
|
+
try {
|
|
658
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
659
|
+
timeZone: timezone,
|
|
660
|
+
year: "numeric",
|
|
661
|
+
month: "2-digit",
|
|
662
|
+
day: "2-digit"
|
|
663
|
+
}).formatToParts(instant);
|
|
664
|
+
const fields = Object.fromEntries(parts.map(({ type, value: item }) => [type, item]));
|
|
665
|
+
return `${fields.year}-${fields.month}-${fields.day}`;
|
|
666
|
+
} catch {
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
async function primaryMarkdown(loaded, record) {
|
|
672
|
+
const definition = loaded.model.resources[record.type];
|
|
673
|
+
const item = markdownEntries(loaded.model, record).find((entry) => (
|
|
674
|
+
definition.markdown?.[entry.name]?.primary || entry.name === loaded.model.recordContent?.slot
|
|
675
|
+
));
|
|
676
|
+
if (!item) return "";
|
|
677
|
+
try {
|
|
678
|
+
return await readFile(resolveDataPath(loaded.root, item.path), "utf8");
|
|
679
|
+
} catch {
|
|
680
|
+
return "";
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function initializationNeeded(audit, records, model) {
|
|
685
|
+
const readiness = model.auditReadiness || {};
|
|
686
|
+
const needsDocumentLink = applicableManagementDocuments(audit, readiness)
|
|
687
|
+
.some((definition) => {
|
|
688
|
+
const linked = records.find((record) => record.id === audit[definition.field]);
|
|
689
|
+
return (!linked || linked.template === true) && records.some((record) => (
|
|
690
|
+
record.type === "document" && record.documentKind === definition.kind
|
|
691
|
+
));
|
|
692
|
+
});
|
|
693
|
+
const populationKinds = new Set(records
|
|
694
|
+
.filter((record) => record.type === "audit-population" && record.auditId === audit.id)
|
|
695
|
+
.map((record) => record.populationKind));
|
|
696
|
+
const needsPopulation = audit.auditKind === "soc-2-type-2" && (readiness.populationTemplates || [])
|
|
697
|
+
.some((template) => !populationKinds.has(template.kind));
|
|
698
|
+
return needsDocumentLink || needsPopulation;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function applicableManagementDocuments(audit, readiness) {
|
|
702
|
+
return (readiness.managementDocuments || []).filter((definition) => (
|
|
703
|
+
!audit || !definition.engagementKinds?.length || definition.engagementKinds.includes(audit.auditKind)
|
|
704
|
+
));
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function evidenceOverlaps(record, start, end) {
|
|
708
|
+
return (record.periodStart && record.periodEnd && record.periodStart <= end && record.periodEnd >= start)
|
|
709
|
+
|| (record.collectedOn && record.collectedOn >= start && record.collectedOn <= end);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function evidenceRelevantToAuditDate(record, audit) {
|
|
713
|
+
if (!audit) return false;
|
|
714
|
+
if (audit.auditKind === "soc-2-type-1") {
|
|
715
|
+
const date = audit.typeOneAsOf;
|
|
716
|
+
return Boolean(date) && (
|
|
717
|
+
(record.periodStart && record.periodEnd && record.periodStart <= date && record.periodEnd >= date)
|
|
718
|
+
|| record.collectedOn === date
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
return Boolean(audit.periodStart && audit.periodEnd)
|
|
722
|
+
&& evidenceOverlaps(record, audit.periodStart, audit.periodEnd);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function controlIdsForRecord(record, byId, seen = new Set()) {
|
|
726
|
+
const ids = new Set();
|
|
727
|
+
if (!record || seen.has(record.id)) return ids;
|
|
728
|
+
seen.add(record.id);
|
|
729
|
+
if (record.type === "control") ids.add(record.id);
|
|
730
|
+
for (const id of record.controlIds || []) ids.add(id);
|
|
731
|
+
if (record.controlId) ids.add(record.controlId);
|
|
732
|
+
if (record.obligationId) {
|
|
733
|
+
for (const id of byId.get(record.obligationId)?.controlIds || []) ids.add(id);
|
|
734
|
+
}
|
|
735
|
+
for (const sourceId of record.sourceResourceIds || []) {
|
|
736
|
+
for (const id of controlIdsForRecord(byId.get(sourceId), byId, seen)) ids.add(id);
|
|
737
|
+
}
|
|
738
|
+
if (record.sourceResourceId) {
|
|
739
|
+
for (const id of controlIdsForRecord(byId.get(record.sourceResourceId), byId, seen)) ids.add(id);
|
|
740
|
+
}
|
|
741
|
+
return ids;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function hasMeaningfulValue(value) {
|
|
745
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
746
|
+
return Object.values(value).some((item) => (
|
|
747
|
+
typeof item === "string" ? item.trim() : item && typeof item === "object" ? hasMeaningfulValue(item) : false
|
|
748
|
+
));
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function containsOpenPlaceholder(source) {
|
|
752
|
+
return /\[[^\]\n]{2,}\](?!\()/u.test(source);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function managementDocumentContentIssues(source, definition, audit) {
|
|
756
|
+
if (!source) return ["Add the required Markdown content."];
|
|
757
|
+
if (containsOpenPlaceholder(source)) return ["Replace every bracketed preparation placeholder before approval."];
|
|
758
|
+
const words = source.match(/[\p{L}\p{N}][\p{L}\p{N}'’-]*/gu) || [];
|
|
759
|
+
if (definition.minimumWords && words.length < definition.minimumWords) {
|
|
760
|
+
return [`Add substantive content; this document has ${words.length} words and the preparation check expects at least ${definition.minimumWords}.`];
|
|
761
|
+
}
|
|
762
|
+
const missingHeadings = (definition.requiredHeadings || []).filter((heading) => (
|
|
763
|
+
!new RegExp(`^#{1,6}\\s+.*\\b${escapeRegExp(heading)}\\b`, "imu").test(source)
|
|
764
|
+
));
|
|
765
|
+
if (missingHeadings.length) return [`Add the missing description sections: ${missingHeadings.join(", ")}.`];
|
|
766
|
+
if (definition.dateBinding === "engagement" && audit) {
|
|
767
|
+
const dates = audit.auditKind === "soc-2-type-1"
|
|
768
|
+
? [audit.typeOneAsOf]
|
|
769
|
+
: [audit.periodStart, audit.periodEnd];
|
|
770
|
+
if (dates.some((date) => date && !source.includes(date))) {
|
|
771
|
+
return ["Name the exact engagement date or period in the document."];
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
return [];
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function engagementDocument(template, definition, audit, id) {
|
|
778
|
+
const {
|
|
779
|
+
approvedOn,
|
|
780
|
+
effectiveOn,
|
|
781
|
+
evidenceIds,
|
|
782
|
+
id: ignoredId,
|
|
783
|
+
status: ignoredStatus,
|
|
784
|
+
supersedesId,
|
|
785
|
+
template: ignoredTemplate,
|
|
786
|
+
title: ignoredTitle,
|
|
787
|
+
...shared
|
|
788
|
+
} = template;
|
|
789
|
+
return {
|
|
790
|
+
...shared,
|
|
791
|
+
id,
|
|
792
|
+
title: `${audit.title}: ${definition.title}`,
|
|
793
|
+
status: "draft",
|
|
794
|
+
ownerIds: [...audit.ownerIds],
|
|
795
|
+
...(audit.systemIds?.length ? { systemIds: [...audit.systemIds] } : {}),
|
|
796
|
+
version: "0.1"
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function materializeManagementMarkdown(source, audit, records) {
|
|
801
|
+
const keep = audit.auditKind === "soc-2-type-1" ? "type-1" : "type-2";
|
|
802
|
+
const discard = keep === "type-1" ? "type-2" : "type-1";
|
|
803
|
+
const withoutDiscarded = source.replace(
|
|
804
|
+
new RegExp(`<!-- ${discard}:start -->[\\s\\S]*?<!-- ${discard}:end -->\\s*`, "g"),
|
|
805
|
+
""
|
|
806
|
+
);
|
|
807
|
+
const systems = (audit.systemIds || [])
|
|
808
|
+
.map((id) => records.find((record) => record.id === id)?.title)
|
|
809
|
+
.filter(Boolean)
|
|
810
|
+
.join(", ");
|
|
811
|
+
const categoryTags = new Set(["security", "availability", "processing-integrity", "confidentiality", "privacy"]);
|
|
812
|
+
const categories = [...new Set((audit.requirementIds || [])
|
|
813
|
+
.flatMap((id) => records.find((record) => record.id === id)?.tags || [])
|
|
814
|
+
.filter((tag) => categoryTags.has(tag))
|
|
815
|
+
.map(displayValue))]
|
|
816
|
+
.join(", ");
|
|
817
|
+
const period = audit.auditKind === "soc-2-type-1"
|
|
818
|
+
? audit.typeOneAsOf || "[as-of date]"
|
|
819
|
+
: audit.periodStart && audit.periodEnd
|
|
820
|
+
? `${audit.periodStart} through ${audit.periodEnd}`
|
|
821
|
+
: "[start date] through [end date]";
|
|
822
|
+
return withoutDiscarded
|
|
823
|
+
.replaceAll(`<!-- ${keep}:start -->`, "")
|
|
824
|
+
.replaceAll(`<!-- ${keep}:end -->`, "")
|
|
825
|
+
.replaceAll("[as-of date]", audit.typeOneAsOf || "[as-of date]")
|
|
826
|
+
.replaceAll("[start date]", audit.periodStart || "[start date]")
|
|
827
|
+
.replaceAll("[end date]", audit.periodEnd || "[end date]")
|
|
828
|
+
.replaceAll("[engagement date or period]", period)
|
|
829
|
+
.replaceAll("[engagement scope]", audit.scope || "[engagement scope]")
|
|
830
|
+
.replaceAll("[in-scope systems]", systems || "[in-scope systems]")
|
|
831
|
+
.replaceAll("[selected categories]", categories || "[selected categories]")
|
|
832
|
+
.replaceAll(
|
|
833
|
+
"[Carve-out, inclusive, or not applicable]",
|
|
834
|
+
audit.subserviceMethod ? displayValue(audit.subserviceMethod) : "[Carve-out, inclusive, or not applicable]"
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function escapeRegExp(value) {
|
|
839
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function partiesIndependent(ownerIds, approverIds, byId) {
|
|
843
|
+
const owners = partyPeople(ownerIds, byId);
|
|
844
|
+
const approvers = partyPeople(approverIds, byId);
|
|
845
|
+
return owners.size > 0
|
|
846
|
+
&& approvers.size > 0
|
|
847
|
+
&& ![...owners].some((id) => approvers.has(id));
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function partyPeople(ids = [], byId, seen = new Set()) {
|
|
851
|
+
const people = new Set();
|
|
852
|
+
for (const id of ids) {
|
|
853
|
+
if (seen.has(id)) continue;
|
|
854
|
+
seen.add(id);
|
|
855
|
+
const record = byId.get(id);
|
|
856
|
+
if (record?.type === "person") people.add(id);
|
|
857
|
+
if (record?.type === "team") {
|
|
858
|
+
for (const personId of partyPeople([...(record.memberIds || []), ...(record.chairIds || [])], byId, seen)) {
|
|
859
|
+
people.add(personId);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
return people;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function auditSummary(audit) {
|
|
867
|
+
return {
|
|
868
|
+
id: audit.id,
|
|
869
|
+
title: audit.title,
|
|
870
|
+
status: audit.status,
|
|
871
|
+
kind: audit.auditKind,
|
|
872
|
+
periodStart: audit.periodStart || null,
|
|
873
|
+
periodEnd: audit.periodEnd || null
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function stage(id, title, description, items) {
|
|
878
|
+
return { id, title, description, items };
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function item(id, status, title, message, resource = {}) {
|
|
882
|
+
return {
|
|
883
|
+
id,
|
|
884
|
+
status,
|
|
885
|
+
title,
|
|
886
|
+
message,
|
|
887
|
+
...(resource.type ? { resourceType: resource.type } : {}),
|
|
888
|
+
...(resource.id ? { resourceId: resource.id } : {})
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function countStatuses(items) {
|
|
893
|
+
const counts = { complete: 0, action: 0, later: 0, external: 0, info: 0 };
|
|
894
|
+
for (const item of items) counts[item.status] = (counts[item.status] || 0) + 1;
|
|
895
|
+
return counts;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function displayValue(value) {
|
|
899
|
+
return String(value || "not started").replaceAll("-", " ").replace(/\b\w/g, (character) => character.toUpperCase());
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function shiftYear(value, offset) {
|
|
903
|
+
const date = new Date(`${value}T00:00:00Z`);
|
|
904
|
+
date.setUTCFullYear(date.getUTCFullYear() + offset);
|
|
905
|
+
return date.toISOString().slice(0, 10);
|
|
906
|
+
}
|