filegrc 0.1.0 → 0.2.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 +19 -1
- package/model/v1.json +159 -165
- package/package.json +1 -1
- package/src/agent.js +4 -0
- package/src/audit-preparation.js +217 -116
- package/src/cli.js +264 -7
- package/src/evidence-packet.js +72 -19
- package/src/evidence-tests.js +69 -0
- package/src/git.js +1 -0
- package/src/index.js +11 -0
- package/src/model-docs.js +51 -6
- package/src/obligations.js +87 -11
- package/src/program-lifecycle.js +22 -0
- package/src/program-path.js +275 -0
- package/src/program-readiness.js +635 -0
- package/src/resource-markdown.js +11 -4
- package/src/server.js +8 -0
- package/src/setup.js +187 -0
- package/src/state.js +8 -1
- package/src/validate.js +67 -5
- package/src/web.js +757 -428
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createResourceId } from "./id.js";
|
|
2
2
|
import { markdownEntries } from "./resource-markdown.js";
|
|
3
|
+
import { RESOURCE_INSTRUCTIONS, resourceProgramContext } from "./program-path.js";
|
|
3
4
|
|
|
4
5
|
const STARTING_STATUS_ORDER = [
|
|
5
6
|
"draft",
|
|
@@ -73,7 +74,10 @@ export function buildAgentGuide(loaded, type, options = {}) {
|
|
|
73
74
|
type,
|
|
74
75
|
title: definition.title,
|
|
75
76
|
pluralTitle: definition.pluralTitle,
|
|
77
|
+
instructions: RESOURCE_INSTRUCTIONS[type] || definition.description,
|
|
78
|
+
use: definition.description,
|
|
76
79
|
purpose: definition.description,
|
|
80
|
+
programStep: resourceProgramContext(type),
|
|
77
81
|
policyBasis: definition.guidance.policyBasis,
|
|
78
82
|
cadence: definition.guidance.cadence,
|
|
79
83
|
policySourceIds: definition.guidance.sourceResourceIds ?? [],
|
package/src/audit-preparation.js
CHANGED
|
@@ -3,9 +3,34 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import { createResource, createResources, deleteResource, updateResource } from "./files.js";
|
|
4
4
|
import { createResourceId } from "./id.js";
|
|
5
5
|
import { resolveDataPath } from "./paths.js";
|
|
6
|
+
import { assessProgramReadiness } from "./program-readiness.js";
|
|
6
7
|
import { markdownEntries } from "./resource-markdown.js";
|
|
7
8
|
import { loadWorkspace } from "./workspace.js";
|
|
8
9
|
|
|
10
|
+
const NON_EVIDENCE_RECORD_TYPES = new Set([
|
|
11
|
+
"audit",
|
|
12
|
+
"audit-population",
|
|
13
|
+
"audit-request",
|
|
14
|
+
"commitment",
|
|
15
|
+
"complementary-control",
|
|
16
|
+
"control",
|
|
17
|
+
"control-test",
|
|
18
|
+
"document",
|
|
19
|
+
"evidence",
|
|
20
|
+
"framework",
|
|
21
|
+
"obligation",
|
|
22
|
+
"organization",
|
|
23
|
+
"person",
|
|
24
|
+
"policy",
|
|
25
|
+
"renderer-settings",
|
|
26
|
+
"requirement",
|
|
27
|
+
"system",
|
|
28
|
+
"team",
|
|
29
|
+
"training",
|
|
30
|
+
"vendor",
|
|
31
|
+
"workspace"
|
|
32
|
+
]);
|
|
33
|
+
|
|
9
34
|
export async function assessAuditPreparation(input, options = {}) {
|
|
10
35
|
const loaded = input?.resources && input?.model && input?.entries
|
|
11
36
|
? input
|
|
@@ -20,13 +45,23 @@ export async function assessAuditPreparation(input, options = {}) {
|
|
|
20
45
|
: audits.find((record) => !["complete", "closed", "canceled"].includes(record.status)) || audits[0];
|
|
21
46
|
if (options.auditId && !audit) throw new Error(`Audit "${options.auditId}" was not found.`);
|
|
22
47
|
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
stages
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
48
|
+
const programReadiness = options.programReadiness || await assessProgramReadiness(loaded, {
|
|
49
|
+
generatedAt: options.generatedAt
|
|
50
|
+
});
|
|
51
|
+
const stages = [
|
|
52
|
+
programFoundationStage(programReadiness, loaded.workspace),
|
|
53
|
+
engagementStage(audit, byId, programReadiness),
|
|
54
|
+
scopeStage(audit, records, byId, programReadiness)
|
|
55
|
+
];
|
|
56
|
+
const fieldworkSections = audit
|
|
57
|
+
? [
|
|
58
|
+
await documentsStage(loaded, audit, byId),
|
|
59
|
+
evidenceStage(audit, records, byId, loaded.model),
|
|
60
|
+
populationsStage(audit, records, byId, loaded.model)
|
|
61
|
+
]
|
|
62
|
+
: [];
|
|
63
|
+
stages.push(fieldworkStage(audit, fieldworkSections));
|
|
64
|
+
stages.push(auditorStage());
|
|
30
65
|
|
|
31
66
|
for (const stage of stages) {
|
|
32
67
|
stage.counts = countStatuses(stage.items);
|
|
@@ -40,7 +75,7 @@ export async function assessAuditPreparation(input, options = {}) {
|
|
|
40
75
|
schemaVersion: 1,
|
|
41
76
|
generatedAt: options.generatedAt || new Date().toISOString(),
|
|
42
77
|
audit: audit ? auditSummary(audit) : null,
|
|
43
|
-
status:
|
|
78
|
+
status: !audit ? "not-started" : counts.action ? "needs-work" : "management-ready",
|
|
44
79
|
progress: {
|
|
45
80
|
complete: completedManagedItems.length,
|
|
46
81
|
total: managedItems.length,
|
|
@@ -169,19 +204,17 @@ export async function prepareAuditWorkspace(input, options = {}) {
|
|
|
169
204
|
};
|
|
170
205
|
}
|
|
171
206
|
|
|
172
|
-
function scopeStage(audit, records, byId) {
|
|
207
|
+
function scopeStage(audit, records, byId, programReadiness) {
|
|
173
208
|
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
209
|
if (!audit) {
|
|
184
|
-
|
|
210
|
+
items.push(item(
|
|
211
|
+
"formal-period",
|
|
212
|
+
"later",
|
|
213
|
+
"Confirm the formal report scope and period",
|
|
214
|
+
"Create the engagement after selecting a CPA firm. Keep operating the management program and preserving evidence in the meantime.",
|
|
215
|
+
{ type: "audit" }
|
|
216
|
+
));
|
|
217
|
+
return stage("period", "Confirm the Formal Period", "Record the auditor-agreed report scope and dates without overwriting management's candidate period.", items);
|
|
185
218
|
}
|
|
186
219
|
|
|
187
220
|
const periodComplete = audit.auditKind === "soc-2-type-2"
|
|
@@ -192,11 +225,11 @@ function scopeStage(audit, records, byId) {
|
|
|
192
225
|
items.push(item(
|
|
193
226
|
"period",
|
|
194
227
|
periodComplete ? "complete" : "action",
|
|
195
|
-
"Set the report type and date",
|
|
228
|
+
"Set the auditor-agreed report type and date",
|
|
196
229
|
periodComplete
|
|
197
230
|
? audit.auditKind === "soc-2-type-2"
|
|
198
|
-
? `Type 2 period: ${audit.periodStart} through ${audit.periodEnd}.`
|
|
199
|
-
: `Type 1 as-of date: ${audit.typeOneAsOf}.`
|
|
231
|
+
? `Auditor-agreed Type 2 period: ${audit.periodStart} through ${audit.periodEnd}.`
|
|
232
|
+
: `Auditor-agreed Type 1 as-of date: ${audit.typeOneAsOf}.`
|
|
200
233
|
: audit.auditKind === "soc-2-type-1"
|
|
201
234
|
? "Set the Type 1 as-of date."
|
|
202
235
|
: audit.auditKind === "soc-2-type-2"
|
|
@@ -204,6 +237,21 @@ function scopeStage(audit, records, byId) {
|
|
|
204
237
|
: "Change this readiness record to a Type 1 or Type 2 engagement before planning the report.",
|
|
205
238
|
audit
|
|
206
239
|
));
|
|
240
|
+
if (audit.auditKind === "soc-2-type-2" && programReadiness.target.candidatePeriodStart) {
|
|
241
|
+
const candidate = [programReadiness.target.candidatePeriodStart, programReadiness.target.candidatePeriodEnd]
|
|
242
|
+
.filter(Boolean)
|
|
243
|
+
.join(" through ");
|
|
244
|
+
const agreed = [audit.periodStart, audit.periodEnd].filter(Boolean).join(" through ");
|
|
245
|
+
items.push(item(
|
|
246
|
+
"candidate-period-comparison",
|
|
247
|
+
"info",
|
|
248
|
+
"Compare candidate and auditor-agreed periods",
|
|
249
|
+
agreed
|
|
250
|
+
? `Management candidate: ${candidate}. Auditor agreed: ${agreed}. Preserve both sets of dates when they differ.`
|
|
251
|
+
: `Management candidate: ${candidate}. The formal period remains unset until the CPA firm agrees to it.`,
|
|
252
|
+
audit
|
|
253
|
+
));
|
|
254
|
+
}
|
|
207
255
|
|
|
208
256
|
const systems = (audit.systemIds || []).map((id) => byId.get(id)).filter(Boolean);
|
|
209
257
|
const completeSystems = systems.filter((system) => (
|
|
@@ -350,87 +398,79 @@ function scopeStage(audit, records, byId) {
|
|
|
350
398
|
audit
|
|
351
399
|
));
|
|
352
400
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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.", [
|
|
401
|
+
return stage("period", "Confirm the Formal Period", "Record the auditor-agreed report type, date or period, scope, criteria, systems, and dependency treatment.", items);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function programFoundationStage(programReadiness, workspace) {
|
|
405
|
+
const ready = programReadiness.evidenceReady;
|
|
406
|
+
return stage("program", "Program Readiness", "The management program can be prepared and operated without an audit record or CPA firm.", [
|
|
404
407
|
item(
|
|
405
|
-
"
|
|
406
|
-
|
|
407
|
-
"
|
|
408
|
-
|
|
409
|
-
? `${
|
|
410
|
-
:
|
|
411
|
-
|
|
412
|
-
)
|
|
408
|
+
"evidence-ready",
|
|
409
|
+
ready ? "complete" : "action",
|
|
410
|
+
"Reach the Evidence Ready gate",
|
|
411
|
+
ready
|
|
412
|
+
? `${programReadiness.target.label} is evidence-ready. ${programReadiness.operating ? "Evidence collection is running." : "Management can begin the candidate period."}`
|
|
413
|
+
: `${programReadiness.counts.action} program-readiness actions remain across scope, policies, controls, and evidence preparation.`,
|
|
414
|
+
workspace || { type: "workspace" }
|
|
415
|
+
)
|
|
416
|
+
]);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function engagementStage(audit, byId, programReadiness) {
|
|
420
|
+
if (!audit) {
|
|
421
|
+
return stage("engagement", "Engage the Auditor", "Select a CPA firm after the program is evidence-ready, or earlier when a customer deadline makes coordination urgent.", [
|
|
422
|
+
item(
|
|
423
|
+
"engagement",
|
|
424
|
+
programReadiness.evidenceReady ? "action" : "later",
|
|
425
|
+
"Create the CPA engagement",
|
|
426
|
+
programReadiness.evidenceReady
|
|
427
|
+
? "Select the independent CPA firm, sign the engagement, then create the audit record with the firm and contacts."
|
|
428
|
+
: "Finish the management program first. Early auditor engagement remains available when a customer deadline requires it.",
|
|
429
|
+
{ type: "audit" }
|
|
430
|
+
)
|
|
431
|
+
]);
|
|
432
|
+
}
|
|
433
|
+
const auditor = audit.auditorVendorId ? byId.get(audit.auditorVendorId) : null;
|
|
434
|
+
const named = Boolean(auditor || hasMeaningfulValue(audit.auditor));
|
|
435
|
+
return stage("engagement", "Engage the Auditor", "Record the independent CPA firm and engagement contacts before treating the audit as active.", [
|
|
413
436
|
item(
|
|
414
|
-
"
|
|
415
|
-
|
|
416
|
-
"
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
: "Select the controls for the engagement, then confirm each statement matches current practice.",
|
|
420
|
-
implementedControls[0] || selectedControls[0] || { type: "control" }
|
|
437
|
+
"engagement-record",
|
|
438
|
+
"complete",
|
|
439
|
+
"Create the engagement record",
|
|
440
|
+
`${audit.title} tracks the formal scope, dates, requests, fieldwork, and report.`,
|
|
441
|
+
audit
|
|
421
442
|
),
|
|
422
443
|
item(
|
|
423
|
-
"
|
|
424
|
-
|
|
425
|
-
"
|
|
426
|
-
|
|
427
|
-
? `${
|
|
428
|
-
: "
|
|
429
|
-
|
|
444
|
+
"auditor",
|
|
445
|
+
named ? "complete" : "action",
|
|
446
|
+
"Record the independent CPA firm",
|
|
447
|
+
named
|
|
448
|
+
? `${auditor?.title || audit.auditor?.firm || "The independent CPA firm"} is recorded for the engagement.`
|
|
449
|
+
: "Select the CPA firm and record it here. The independent management policy reviewer is a different role.",
|
|
450
|
+
audit
|
|
430
451
|
)
|
|
431
452
|
]);
|
|
432
453
|
}
|
|
433
454
|
|
|
455
|
+
function fieldworkStage(audit, sections) {
|
|
456
|
+
if (!audit) {
|
|
457
|
+
return stage("fieldwork", "Prepare Fieldwork", "Build engagement-specific documents, exact-period evidence, and Type 2 populations after the firm and period are recorded.", [
|
|
458
|
+
item("fieldwork-later", "later", "Prepare engagement-specific fieldwork", "This work starts after the CPA engagement and formal period exist.", { type: "audit" })
|
|
459
|
+
]);
|
|
460
|
+
}
|
|
461
|
+
const items = sections.flatMap((section) => section.items.map((current) => ({
|
|
462
|
+
...current,
|
|
463
|
+
id: `${section.id}-${current.id}`,
|
|
464
|
+
section: section.title
|
|
465
|
+
})));
|
|
466
|
+
return stage(
|
|
467
|
+
"fieldwork",
|
|
468
|
+
"Prepare Fieldwork",
|
|
469
|
+
"Complete management documents, exact-period operating evidence, and Type 2 population reconciliations for the engagement.",
|
|
470
|
+
items
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
434
474
|
async function documentsStage(loaded, audit, byId) {
|
|
435
475
|
const definitions = applicableManagementDocuments(audit, loaded.model.auditReadiness || {});
|
|
436
476
|
const items = [];
|
|
@@ -500,27 +540,55 @@ async function documentsStage(loaded, audit, byId) {
|
|
|
500
540
|
function evidenceStage(audit, records, byId, model) {
|
|
501
541
|
const controls = (audit?.controlIds || []).map((id) => byId.get(id)).filter(Boolean);
|
|
502
542
|
const evidence = records.filter((record) => record.type === "evidence");
|
|
503
|
-
const
|
|
543
|
+
const externalEvidence = evidence.filter((record) => (
|
|
504
544
|
record.status === "verified"
|
|
505
545
|
&& (record.evidenceKind !== "rendered-record" || record.sourceCommit)
|
|
506
546
|
&& evidenceRelevantToAuditDate(record, audit)
|
|
507
547
|
));
|
|
508
|
-
const
|
|
548
|
+
const managedFamilies = (model.evidenceSourceFamilies || []).filter((family) => family.collectionTestRequired === false);
|
|
549
|
+
const externalFamilies = (model.evidenceSourceFamilies || []).filter((family) => family.collectionTestRequired !== false);
|
|
550
|
+
const evidenceFamiliesFor = (control) => (model.evidenceSourceFamilies || []).filter((family) => (
|
|
551
|
+
(family.controlCodes || []).includes(control.code)
|
|
552
|
+
));
|
|
553
|
+
const fileGRCRecords = records.filter((record) => (
|
|
554
|
+
!NON_EVIDENCE_RECORD_TYPES.has(record.type)
|
|
555
|
+
&& controlIdsForRecord(record, byId).size
|
|
556
|
+
&& recordRelevantToAuditDate(record, audit, model)
|
|
557
|
+
));
|
|
558
|
+
const managedControls = controls.filter((control) => managedFamilies.some((family) => (
|
|
559
|
+
(family.controlCodes || []).includes(control.code)
|
|
560
|
+
)));
|
|
561
|
+
const controlsWithFileGRCRecords = managedControls.filter((control) => fileGRCRecords.some((record) => (
|
|
562
|
+
controlIdsForRecord(record, byId).has(control.id)
|
|
563
|
+
)));
|
|
564
|
+
const externalControls = controls.filter((control) => externalFamilies.some((family) => (
|
|
565
|
+
(family.controlCodes || []).includes(control.code)
|
|
566
|
+
)) || !evidenceFamiliesFor(control).length);
|
|
567
|
+
const controlsWithExternalEvidence = externalControls.filter((control) => externalEvidence.some((record) => (
|
|
509
568
|
controlIdsForRecord(record, byId).has(control.id)
|
|
510
569
|
)));
|
|
511
570
|
const items = [
|
|
512
571
|
item(
|
|
513
|
-
"
|
|
514
|
-
|
|
515
|
-
"
|
|
516
|
-
|
|
517
|
-
? `${
|
|
518
|
-
: "
|
|
519
|
-
|
|
572
|
+
"filegrc-evidence",
|
|
573
|
+
managedControls.length && controlsWithFileGRCRecords.length === managedControls.length ? "complete" : managedControls.length ? "action" : "info",
|
|
574
|
+
"Review FileGRC Evidence",
|
|
575
|
+
managedControls.length
|
|
576
|
+
? `${controlsWithFileGRCRecords.length} of ${managedControls.length} selected controls that use FileGRC workflows have a dated operating record for the formal period. Complete each Step 5 record, link it to the control, and add results in its structured fields or Markdown.`
|
|
577
|
+
: "No selected controls use a dedicated FileGRC operating record.",
|
|
578
|
+
fileGRCRecords[0] || { type: managedFamilies[0]?.operationRecordTypes?.[0] || "control" }
|
|
579
|
+
),
|
|
580
|
+
item(
|
|
581
|
+
"external-evidence",
|
|
582
|
+
externalControls.length && controlsWithExternalEvidence.length === externalControls.length ? "complete" : externalControls.length ? "action" : "info",
|
|
583
|
+
"Review External Evidence",
|
|
584
|
+
externalControls.length
|
|
585
|
+
? `${controlsWithExternalEvidence.length} of ${externalControls.length} selected controls that rely on external systems have verified External Evidence for the formal period. Confirm the source System, date or period, control links, collector, verifier, and retained artifact or approved external reference.`
|
|
586
|
+
: "No selected controls require a separate External Evidence record.",
|
|
587
|
+
externalEvidence[0] || { type: "evidence" }
|
|
520
588
|
)
|
|
521
589
|
];
|
|
522
590
|
const systems = records.filter((record) => record.type === "system" && record.status === "active");
|
|
523
|
-
for (const source of model.
|
|
591
|
+
for (const source of model.evidenceSourceFamilies || []) {
|
|
524
592
|
const relevantControls = controls.filter((control) => (source.controlCodes || []).includes(control.code));
|
|
525
593
|
if (!relevantControls.length) {
|
|
526
594
|
items.push(item(
|
|
@@ -531,10 +599,28 @@ function evidenceStage(audit, records, byId, model) {
|
|
|
531
599
|
));
|
|
532
600
|
continue;
|
|
533
601
|
}
|
|
602
|
+
if (source.collectionTestRequired === false) {
|
|
603
|
+
const sourceRecords = fileGRCRecords.filter((record) => (
|
|
604
|
+
relevantControls.some((control) => controlIdsForRecord(record, byId).has(control.id))
|
|
605
|
+
));
|
|
606
|
+
const coveredControls = relevantControls.filter((control) => sourceRecords.some((record) => (
|
|
607
|
+
controlIdsForRecord(record, byId).has(control.id)
|
|
608
|
+
)));
|
|
609
|
+
items.push(item(
|
|
610
|
+
`filegrc-${source.id}`,
|
|
611
|
+
coveredControls.length === relevantControls.length ? "complete" : "action",
|
|
612
|
+
source.title,
|
|
613
|
+
coveredControls.length === relevantControls.length
|
|
614
|
+
? `${sourceRecords.length} dated FileGRC ${sourceRecords.length === 1 ? "record" : "records"} cover ${relevantControls.length} mapped controls. External artifacts needed to support those results are linked from the operating records.`
|
|
615
|
+
: `${coveredControls.length} of ${relevantControls.length} mapped controls have a dated ${source.operationRecordTypes.map(displayValue).join(" or ")} record for the formal period. Complete the Step 5 work and attach or reference any supporting external artifact on that record.`,
|
|
616
|
+
sourceRecords[0] || { type: source.operationRecordTypes[0] }
|
|
617
|
+
));
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
534
620
|
const sourceSystems = systems.filter((system) => (
|
|
535
621
|
(system.evidenceSourceKinds || []).some((kind) => (source.sourceKinds || []).includes(kind))
|
|
536
622
|
));
|
|
537
|
-
const coveredControls = relevantControls.filter((control) =>
|
|
623
|
+
const coveredControls = relevantControls.filter((control) => externalEvidence.some((record) => (
|
|
538
624
|
sourceSystems.some((system) => system.id === record.sourceSystemId)
|
|
539
625
|
&& controlIdsForRecord(record, byId).has(control.id)
|
|
540
626
|
)));
|
|
@@ -549,12 +635,12 @@ function evidenceStage(audit, records, byId, model) {
|
|
|
549
635
|
status,
|
|
550
636
|
source.title,
|
|
551
637
|
message,
|
|
552
|
-
|
|
638
|
+
externalEvidence.find((record) => sourceSystems.some((system) => system.id === record.sourceSystemId))
|
|
553
639
|
|| sourceSystems[0]
|
|
554
640
|
|| { type: "system" }
|
|
555
641
|
));
|
|
556
642
|
}
|
|
557
|
-
return stage("evidence", "
|
|
643
|
+
return stage("evidence", "Audit Evidence", "Review both evidence paths: dated FileGRC operating records and verified External Evidence from authoritative systems. FileGRC includes both in the audit packet.", items);
|
|
558
644
|
}
|
|
559
645
|
|
|
560
646
|
function populationsStage(audit, records, byId, model) {
|
|
@@ -589,8 +675,8 @@ function populationsStage(audit, records, byId, model) {
|
|
|
589
675
|
);
|
|
590
676
|
}
|
|
591
677
|
|
|
592
|
-
function auditorStage(
|
|
593
|
-
return stage("auditor", "
|
|
678
|
+
function auditorStage() {
|
|
679
|
+
return stage("auditor", "Fieldwork and Report", "FileGRC prepares the record set but does not make the CPA firm's independent judgments.", [
|
|
594
680
|
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
681
|
item("sampling", "external", "Sample selection and independent testing", "The auditor chooses samples, performs tests, evaluates exceptions, and decides whether more work is needed."),
|
|
596
682
|
item("report", "external", "Report and opinion", "Management reviews and signs its representations. The auditor issues the final report and opinion."),
|
|
@@ -722,6 +808,27 @@ function evidenceRelevantToAuditDate(record, audit) {
|
|
|
722
808
|
&& evidenceOverlaps(record, audit.periodStart, audit.periodEnd);
|
|
723
809
|
}
|
|
724
810
|
|
|
811
|
+
function recordRelevantToAuditDate(record, audit, model) {
|
|
812
|
+
if (!audit) return false;
|
|
813
|
+
const start = audit.auditKind === "soc-2-type-1" ? audit.typeOneAsOf : audit.periodStart;
|
|
814
|
+
const end = audit.auditKind === "soc-2-type-1" ? audit.typeOneAsOf : audit.periodEnd;
|
|
815
|
+
if (!start || !end) return false;
|
|
816
|
+
if (record.periodStart && record.periodEnd && record.periodStart <= end && record.periodEnd >= start) {
|
|
817
|
+
return true;
|
|
818
|
+
}
|
|
819
|
+
const definition = model.resources[record.type];
|
|
820
|
+
const fields = { ...model.commonFields, ...(definition?.fields || {}) };
|
|
821
|
+
return Object.entries(fields).some(([name, field]) => {
|
|
822
|
+
const value = record[name];
|
|
823
|
+
if (field.type === "date") return value >= start && value <= end;
|
|
824
|
+
if (field.type === "timestamp" && typeof value === "string") {
|
|
825
|
+
const date = value.slice(0, 10);
|
|
826
|
+
return date >= start && date <= end;
|
|
827
|
+
}
|
|
828
|
+
return false;
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
|
|
725
832
|
function controlIdsForRecord(record, byId, seen = new Set()) {
|
|
726
833
|
const ids = new Set();
|
|
727
834
|
if (!record || seen.has(record.id)) return ids;
|
|
@@ -898,9 +1005,3 @@ function countStatuses(items) {
|
|
|
898
1005
|
function displayValue(value) {
|
|
899
1006
|
return String(value || "not started").replaceAll("-", " ").replace(/\b\w/g, (character) => character.toUpperCase());
|
|
900
1007
|
}
|
|
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
|
-
}
|