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