filegrc 0.7.0 → 0.8.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 +14 -6
- package/model/index.js +21 -4
- package/model/v4.json +97 -6
- package/model/v5.json +10233 -0
- package/package.json +4 -2
- package/src/agent.js +4 -3
- package/src/audit-preparation.js +635 -81
- package/src/audit-transition.js +8 -2
- package/src/batch-review.js +21 -11
- package/src/cli.js +117 -12
- package/src/collection-review.js +4 -3
- package/src/collection-scope.js +4 -3
- package/src/document-activation.js +145 -0
- package/src/evidence-packet.js +393 -106
- package/src/external-reviewer.js +5 -4
- package/src/files.js +194 -35
- package/src/git.js +106 -10
- package/src/index.js +10 -1
- package/src/model-migration.js +218 -7
- package/src/obligations.js +14 -11
- package/src/policy-library/information-security-policy-v2.md +290 -0
- package/src/policy-library.js +827 -0
- package/src/program-lifecycle.js +93 -3
- package/src/program-path.js +14 -9
- package/src/program-readiness.js +306 -75
- package/src/program.js +5 -3
- package/src/reconciliation.js +3 -2
- package/src/server.js +29 -7
- package/src/setup.js +25 -5
- package/src/soc2.js +228 -0
- package/src/state.js +8 -0
- package/src/validate.js +168 -14
- package/src/web.js +257 -35
- package/src/workflow.js +107 -44
- package/src/workspace.js +5 -0
package/src/evidence-packet.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { createReadStream } from "node:fs";
|
|
3
3
|
import { copyFile, mkdir, readFile, readdir, readlink, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import { modelSupports } from "../model/index.js";
|
|
5
6
|
import { assessAuditPreparation } from "./audit-preparation.js";
|
|
6
7
|
import {
|
|
7
8
|
coverageContains,
|
|
@@ -10,13 +11,26 @@ import {
|
|
|
10
11
|
coverageOverlaps,
|
|
11
12
|
coverageStart
|
|
12
13
|
} from "./coverage.js";
|
|
13
|
-
import {
|
|
14
|
+
import { getFilesAtRevisions, getGitSummary, getWorkspaceHistories, getWorkspaceRevisionSnapshot, hasGitRevision } from "./git.js";
|
|
14
15
|
import { planObligations } from "./obligations.js";
|
|
15
|
-
import { isWithin, resolveDataPath, resolveWorkspacePath } from "./paths.js";
|
|
16
|
+
import { isWithin, resolveDataPath, resolveWorkspacePath, resolveWorkspaceRoot } from "./paths.js";
|
|
16
17
|
import { parseCalendarDate } from "./recurrence.js";
|
|
17
18
|
import { markdownEntries } from "./resource-markdown.js";
|
|
18
19
|
import { serializeWorkspaceMutation } from "./mutation.js";
|
|
20
|
+
import {
|
|
21
|
+
auditorWasEngaged,
|
|
22
|
+
missingSoc2References,
|
|
23
|
+
recordWasInUseDuringAudit,
|
|
24
|
+
REQUIRED_SOC2_DESCRIPTION_REFERENCES,
|
|
25
|
+
REQUIRED_SOC2_SECURITY_REFERENCES,
|
|
26
|
+
signatoryAppointmentIssue,
|
|
27
|
+
soc2ReportEvidenceIssue,
|
|
28
|
+
subsequentEventsReviewIssue
|
|
29
|
+
} from "./soc2.js";
|
|
19
30
|
import { validateWorkspace } from "./validate.js";
|
|
31
|
+
import { measureTiming } from "./timing.js";
|
|
32
|
+
|
|
33
|
+
const preparedPacketValidations = new WeakMap();
|
|
20
34
|
|
|
21
35
|
const NON_EVIDENCE_RECORD_TYPES = new Set([
|
|
22
36
|
"appointment",
|
|
@@ -42,7 +56,6 @@ const NON_EVIDENCE_RECORD_TYPES = new Set([
|
|
|
42
56
|
"vendor",
|
|
43
57
|
"workspace"
|
|
44
58
|
]);
|
|
45
|
-
|
|
46
59
|
export async function prepareEvidencePacket(input, options = {}) {
|
|
47
60
|
const validation = await validateWorkspace(input);
|
|
48
61
|
if (!validation.ok) throw new Error(`The workspace has ${validation.counts.errors} validation ${validation.counts.errors === 1 ? "error" : "errors"}. Fix them before generating evidence.`);
|
|
@@ -99,7 +112,9 @@ export async function prepareEvidencePacket(input, options = {}) {
|
|
|
99
112
|
...(audit.controlIds || []),
|
|
100
113
|
...(audit.contactIds || []),
|
|
101
114
|
...(audit.complementaryControlIds || []),
|
|
102
|
-
...(audit
|
|
115
|
+
...auditSubserviceVendorIds(audit),
|
|
116
|
+
...auditSubserviceComponentIds(audit),
|
|
117
|
+
audit.engagementTermsDocumentId,
|
|
103
118
|
audit.systemDescriptionDocumentId,
|
|
104
119
|
audit.managementAssertionDocumentId,
|
|
105
120
|
audit.managementRepresentationDocumentId,
|
|
@@ -218,7 +233,7 @@ export async function prepareEvidencePacket(input, options = {}) {
|
|
|
218
233
|
return sourceRevisionValidity.get(revision);
|
|
219
234
|
};
|
|
220
235
|
const evidence = [...evidenceIds].map((id) => evidenceSummary(byId.get(id), byId, revisionIsValid)).filter(Boolean).sort(byTitle);
|
|
221
|
-
const v4 =
|
|
236
|
+
const v4 = modelSupports(loaded.model, "component-sources");
|
|
222
237
|
const sourceSystemIds = new Set([
|
|
223
238
|
...(v4
|
|
224
239
|
? [...controlIds].flatMap((id) => byId.get(id)?.evidenceSourceComponentIds || [])
|
|
@@ -234,8 +249,13 @@ export async function prepareEvidencePacket(input, options = {}) {
|
|
|
234
249
|
`data/${entry.relativePath}`,
|
|
235
250
|
...markdownEntries(loaded.model, entry.record).map((markdown) => `data/${markdown.path}`)
|
|
236
251
|
]);
|
|
237
|
-
const historyRevision =
|
|
238
|
-
const histories = getWorkspaceHistories(
|
|
252
|
+
const historyRevision = await getWorkspaceRevisionSnapshot(loaded.root);
|
|
253
|
+
const histories = getWorkspaceHistories(
|
|
254
|
+
loaded.root,
|
|
255
|
+
selectedPaths,
|
|
256
|
+
Number.MAX_SAFE_INTEGER,
|
|
257
|
+
{ strict: Boolean(historyRevision.commit) }
|
|
258
|
+
);
|
|
239
259
|
const packetRecords = [...selectedIds]
|
|
240
260
|
.map((id) => byId.get(id))
|
|
241
261
|
.filter(Boolean)
|
|
@@ -290,6 +310,7 @@ export async function prepareEvidencePacket(input, options = {}) {
|
|
|
290
310
|
generatedAt,
|
|
291
311
|
selectDefault: false
|
|
292
312
|
});
|
|
313
|
+
const documentLifecycles = auditDocumentLifecycleSummaries(audit, loaded.model, byId);
|
|
293
314
|
const gaps = packetGaps({
|
|
294
315
|
audit,
|
|
295
316
|
byId,
|
|
@@ -308,7 +329,7 @@ export async function prepareEvidencePacket(input, options = {}) {
|
|
|
308
329
|
});
|
|
309
330
|
const errorCount = gaps.filter(({ severity }) => severity === "error").length;
|
|
310
331
|
const warningCount = gaps.filter(({ severity }) => severity === "warning").length;
|
|
311
|
-
|
|
332
|
+
const packet = {
|
|
312
333
|
schemaVersion: 1,
|
|
313
334
|
generatedAt,
|
|
314
335
|
period: { start, end, basis },
|
|
@@ -327,7 +348,9 @@ export async function prepareEvidencePacket(input, options = {}) {
|
|
|
327
348
|
systemIds: audit.systemIds || [],
|
|
328
349
|
requirementIds: audit.requirementIds || [],
|
|
329
350
|
controlIds: audit.controlIds || [],
|
|
330
|
-
subserviceMethod: audit
|
|
351
|
+
subserviceMethod: auditSubserviceLabel(audit),
|
|
352
|
+
subserviceConclusion: audit.subserviceConclusion || null,
|
|
353
|
+
subserviceTreatments: audit.subserviceTreatments || []
|
|
331
354
|
} : null,
|
|
332
355
|
workspace: {
|
|
333
356
|
title: loaded.workspace.title,
|
|
@@ -359,6 +382,7 @@ export async function prepareEvidencePacket(input, options = {}) {
|
|
|
359
382
|
eventRuns: eventRuns.length,
|
|
360
383
|
evidence: evidence.length,
|
|
361
384
|
populations: populations.length,
|
|
385
|
+
...(modelSupports(loaded.model, "governed-document-activation") ? { documents: documentLifecycles.length } : {}),
|
|
362
386
|
gaps: gaps.length,
|
|
363
387
|
errors: errorCount,
|
|
364
388
|
warnings: warningCount
|
|
@@ -373,11 +397,64 @@ export async function prepareEvidencePacket(input, options = {}) {
|
|
|
373
397
|
[v4 ? "sourceComponents" : "sourceSystems"]: sourceSystems,
|
|
374
398
|
dataModelVersion: String(loaded.model.modelVersion),
|
|
375
399
|
populations,
|
|
400
|
+
...(modelSupports(loaded.model, "governed-document-activation") ? { documentLifecycles } : {}),
|
|
376
401
|
managementPreparation,
|
|
377
402
|
controlCoverage,
|
|
378
403
|
gaps,
|
|
379
404
|
records: packetRecords
|
|
380
405
|
};
|
|
406
|
+
preparedPacketValidations.set(packet, {
|
|
407
|
+
validation,
|
|
408
|
+
revision: {
|
|
409
|
+
commit: packet.revision.commit,
|
|
410
|
+
branch: packet.revision.branch,
|
|
411
|
+
dataDigest: packet.revision.dataDigest
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
return packet;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function auditDocumentLifecycleSummaries(audit, model, byId) {
|
|
418
|
+
if (!audit || !modelSupports(model, "governed-document-activation")) return [];
|
|
419
|
+
const rolesById = new Map();
|
|
420
|
+
const add = (id, role) => {
|
|
421
|
+
if (!id) return;
|
|
422
|
+
const roles = rolesById.get(id) || [];
|
|
423
|
+
roles.push(role);
|
|
424
|
+
rolesById.set(id, roles);
|
|
425
|
+
};
|
|
426
|
+
add(audit.engagementTermsDocumentId, "Engagement Terms");
|
|
427
|
+
for (const definition of model.auditReadiness?.managementDocuments || []) {
|
|
428
|
+
if (!(definition.engagementKinds || []).includes(audit.auditKind)) continue;
|
|
429
|
+
add(audit[definition.field], definition.title || definition.field);
|
|
430
|
+
}
|
|
431
|
+
for (const id of audit.supplementalDocumentIds || []) add(id, "Supplemental");
|
|
432
|
+
return [...rolesById]
|
|
433
|
+
.map(([id, roles]) => {
|
|
434
|
+
const document = byId.get(id);
|
|
435
|
+
if (document?.type !== "document") return null;
|
|
436
|
+
const personLabels = (ids = []) => ids.map((personId) => byId.get(personId)?.title || personId);
|
|
437
|
+
return {
|
|
438
|
+
id: document.id,
|
|
439
|
+
title: document.title,
|
|
440
|
+
workflowScope: document.workflowScope || null,
|
|
441
|
+
roles: [...new Set(roles)],
|
|
442
|
+
status: document.status,
|
|
443
|
+
ownerIds: document.ownerIds || [],
|
|
444
|
+
approverIds: document.approverIds || [],
|
|
445
|
+
approvers: personLabels(document.approverIds),
|
|
446
|
+
approvedOn: document.approvedOn || null,
|
|
447
|
+
approvedContentRevisions: document.approvedContentRevisions || null,
|
|
448
|
+
activationBasis: document.activationBasis || null,
|
|
449
|
+
activatedByIds: document.activatedByIds || [],
|
|
450
|
+
activators: personLabels(document.activatedByIds),
|
|
451
|
+
activatedOn: document.activatedOn || null,
|
|
452
|
+
activatedContentRevisions: document.activatedContentRevisions || null,
|
|
453
|
+
effectiveOn: document.effectiveOn || null
|
|
454
|
+
};
|
|
455
|
+
})
|
|
456
|
+
.filter(Boolean)
|
|
457
|
+
.sort(byTitle);
|
|
381
458
|
}
|
|
382
459
|
|
|
383
460
|
function expandSupersededPolicyIds(policyIds, byId) {
|
|
@@ -403,7 +480,9 @@ function recordRelevantToAudit(record, audit, byId, seen = new Set()) {
|
|
|
403
480
|
...(audit.controlIds || []),
|
|
404
481
|
...(audit.contactIds || []),
|
|
405
482
|
...(audit.complementaryControlIds || []),
|
|
406
|
-
...(audit
|
|
483
|
+
...auditSubserviceVendorIds(audit),
|
|
484
|
+
...auditSubserviceComponentIds(audit),
|
|
485
|
+
audit.engagementTermsDocumentId,
|
|
407
486
|
audit.systemDescriptionDocumentId,
|
|
408
487
|
audit.managementAssertionDocumentId,
|
|
409
488
|
audit.managementRepresentationDocumentId,
|
|
@@ -483,7 +562,17 @@ export async function writeEvidencePacket(input, packet, options = {}) {
|
|
|
483
562
|
let outputOption = options.output || `.filegrc/evidence-packets/${baseName}`;
|
|
484
563
|
requireDerivedOutputPath(outputOption);
|
|
485
564
|
let output = resolveWorkspacePath(input, outputOption);
|
|
486
|
-
const
|
|
565
|
+
const prepared = preparedPacketValidations.get(packet);
|
|
566
|
+
const preparedValidation = prepared
|
|
567
|
+
&& prepared.validation.loaded.root === resolveWorkspaceRoot(input)
|
|
568
|
+
&& prepared.revision.commit === packet.revision?.commit
|
|
569
|
+
&& prepared.revision.branch === packet.revision?.branch
|
|
570
|
+
&& prepared.revision.dataDigest === packet.revision?.dataDigest
|
|
571
|
+
? prepared.validation
|
|
572
|
+
: null;
|
|
573
|
+
const validation = preparedValidation
|
|
574
|
+
? preparedValidation
|
|
575
|
+
: await validateWorkspace(input);
|
|
487
576
|
if (!validation.ok) throw new Error(`The workspace has ${validation.counts.errors} validation ${validation.counts.errors === 1 ? "error" : "errors"}. Fix them before writing evidence.`);
|
|
488
577
|
await assertPacketSourceState(packet, validation.loaded);
|
|
489
578
|
const entriesById = new Map(validation.loaded.entries.map((entry) => [entry.record.id, entry]));
|
|
@@ -513,9 +602,12 @@ export async function writeEvidencePacket(input, packet, options = {}) {
|
|
|
513
602
|
await writePacketFile(output, "README.md", packetMarkdown(packet), files);
|
|
514
603
|
await writePacketFile(output, "index.html", packetHtml(packet), files);
|
|
515
604
|
await writePacketFile(output, "control-matrix.csv", controlMatrixCsv(packet), files);
|
|
605
|
+
if (modelSupports(packet.dataModelVersion, "governed-document-activation")) {
|
|
606
|
+
await writePacketFile(output, "document-lifecycle-index.csv", documentLifecycleIndexCsv(packet), files);
|
|
607
|
+
}
|
|
516
608
|
await writePacketFile(output, "evidence-index.csv", evidenceIndexCsv(packet), files);
|
|
517
|
-
await writePacketFile(output, packet.dataModelVersion
|
|
518
|
-
await writePacketFile(output, packet.dataModelVersion
|
|
609
|
+
await writePacketFile(output, modelSupports(packet.dataModelVersion, "component-sources") ? "source-component-index.csv" : "source-system-index.csv", sourceSystemIndexCsv(packet), files);
|
|
610
|
+
await writePacketFile(output, modelSupports(packet.dataModelVersion, "evidence-artifacts") ? "evidence-artifact-index.csv" : "external-evidence-index.csv", externalEvidenceIndexCsv(packet), files);
|
|
519
611
|
await writePacketFile(output, "population-index.csv", populationIndexCsv(packet), files);
|
|
520
612
|
await writePacketFile(output, "HANDLING.md", packetHandlingMarkdown(packet), files);
|
|
521
613
|
for (const item of packet.records) {
|
|
@@ -542,12 +634,28 @@ export async function writeEvidencePacket(input, packet, options = {}) {
|
|
|
542
634
|
}
|
|
543
635
|
}
|
|
544
636
|
const historyIndex = [];
|
|
637
|
+
const historicalFiles = [];
|
|
545
638
|
for (const item of packet.records) {
|
|
546
|
-
|
|
639
|
+
collectCommittedVersions(historicalFiles, item, item.path, item.history);
|
|
547
640
|
for (const content of item.contentPaths || []) {
|
|
548
|
-
|
|
641
|
+
collectCommittedVersions(historicalFiles, item, content.path, content.history);
|
|
549
642
|
}
|
|
550
643
|
}
|
|
644
|
+
const historicalSources = getFilesAtRevisions(validation.loaded.root, historicalFiles);
|
|
645
|
+
for (let index = 0; index < historicalFiles.length; index += 1) {
|
|
646
|
+
const source = historicalSources[index];
|
|
647
|
+
if (source === null) continue;
|
|
648
|
+
const { item, sourcePath, history } = historicalFiles[index];
|
|
649
|
+
const exportedPath = join("history", item.type, item.id, history.commit, basename(sourcePath));
|
|
650
|
+
await writePacketFile(output, exportedPath, source, files);
|
|
651
|
+
historyIndex.push({
|
|
652
|
+
resourceId: item.id,
|
|
653
|
+
resourceType: item.type,
|
|
654
|
+
sourcePath,
|
|
655
|
+
exportedPath: exportedPath.split("\\").join("/"),
|
|
656
|
+
...history
|
|
657
|
+
});
|
|
658
|
+
}
|
|
551
659
|
await writePacketFile(output, "history/index.json", `${JSON.stringify(historyIndex, null, 2)}\n`, files);
|
|
552
660
|
await assertPacketSourceState(packet, validation.loaded);
|
|
553
661
|
await writeChecksums(output, files);
|
|
@@ -568,35 +676,34 @@ export function generateEvidencePacket(input, options = {}) {
|
|
|
568
676
|
});
|
|
569
677
|
}
|
|
570
678
|
|
|
571
|
-
|
|
679
|
+
function collectCommittedVersions(target, item, sourcePath, history) {
|
|
572
680
|
for (const revision of history || []) {
|
|
573
|
-
|
|
574
|
-
if (source === null) continue;
|
|
575
|
-
const exportedPath = join("history", item.type, item.id, revision.commit, basename(sourcePath));
|
|
576
|
-
await writePacketFile(output, exportedPath, source, files);
|
|
577
|
-
historyIndex.push({
|
|
578
|
-
resourceId: item.id,
|
|
579
|
-
resourceType: item.type,
|
|
580
|
-
sourcePath,
|
|
581
|
-
exportedPath: exportedPath.split("\\").join("/"),
|
|
582
|
-
...revision
|
|
583
|
-
});
|
|
681
|
+
target.push({ item, sourcePath, relativePath: sourcePath, revision: revision.commit, history: revision });
|
|
584
682
|
}
|
|
585
683
|
}
|
|
586
684
|
|
|
587
685
|
async function writeChecksums(output, files) {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
686
|
+
await measureTiming("packet-output-hash", async () => {
|
|
687
|
+
const paths = [...files].sort();
|
|
688
|
+
const lines = new Array(paths.length);
|
|
689
|
+
let next = 0;
|
|
690
|
+
const workers = Array.from({ length: Math.min(4, paths.length) }, async () => {
|
|
691
|
+
while (next < paths.length) {
|
|
692
|
+
const index = next++;
|
|
693
|
+
const relativePath = paths[index];
|
|
694
|
+
const hash = createHash("sha256");
|
|
695
|
+
for await (const chunk of createReadStream(resolvePacketOutputPath(output, relativePath))) hash.update(chunk);
|
|
696
|
+
lines[index] = `${hash.digest("hex")} ${relativePath}`;
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
await Promise.all(workers);
|
|
700
|
+
await writeFile(resolvePacketOutputPath(output, "SHA256SUMS"), `${lines.join("\n")}\n`, { encoding: "utf8", flag: "wx" });
|
|
701
|
+
});
|
|
595
702
|
}
|
|
596
703
|
|
|
597
704
|
function controlMatrixCsv(packet) {
|
|
598
705
|
return csv([
|
|
599
|
-
["Control ID", "Code", "Control", "Control Statement", "Operating Activity", "Status", "Effective On", "Operation Pattern", "Operation Mode", "System IDs", "Requirement IDs", "Policy IDs", "Risk IDs", "filegrc Evidence IDs", packet.dataModelVersion
|
|
706
|
+
["Control ID", "Code", "Control", "Control Statement", "Operating Activity", "Status", "Effective On", "Operation Pattern", "Operation Mode", "System IDs", "Requirement IDs", "Policy IDs", "Risk IDs", "filegrc Evidence IDs", modelSupports(packet.dataModelVersion, "evidence-artifacts") ? "Evidence Artifact IDs" : "External Evidence IDs", "Control Test IDs", "Test Outcomes", "Population IDs", "Population Counts", "Sample Sizes", "Exception Counts", "Population Evidence IDs", "Sample Evidence IDs"],
|
|
600
707
|
...packet.controlCoverage.map((control) => [
|
|
601
708
|
control.id,
|
|
602
709
|
control.code,
|
|
@@ -625,6 +732,30 @@ function controlMatrixCsv(packet) {
|
|
|
625
732
|
]);
|
|
626
733
|
}
|
|
627
734
|
|
|
735
|
+
function documentLifecycleIndexCsv(packet) {
|
|
736
|
+
return csv([
|
|
737
|
+
["Document ID", "Document", "Workflow Scope", "Audit Roles", "Status", "Owner IDs", "Approver IDs", "Approvers", "Approved On", "Approved Content Revisions", "Activation Basis", "Activator IDs", "Activators", "Activated On", "Activation Content Revisions", "Effective On"],
|
|
738
|
+
...packet.documentLifecycles.map((document) => [
|
|
739
|
+
document.id,
|
|
740
|
+
document.title,
|
|
741
|
+
document.workflowScope,
|
|
742
|
+
document.roles.join("\n"),
|
|
743
|
+
document.status,
|
|
744
|
+
document.ownerIds.join("\n"),
|
|
745
|
+
document.approverIds.join("\n"),
|
|
746
|
+
document.approvers.join("\n"),
|
|
747
|
+
document.approvedOn,
|
|
748
|
+
document.approvedContentRevisions ? JSON.stringify(document.approvedContentRevisions) : "",
|
|
749
|
+
document.activationBasis,
|
|
750
|
+
document.activatedByIds.join("\n"),
|
|
751
|
+
document.activators.join("\n"),
|
|
752
|
+
document.activatedOn,
|
|
753
|
+
document.activatedContentRevisions ? JSON.stringify(document.activatedContentRevisions) : "",
|
|
754
|
+
document.effectiveOn
|
|
755
|
+
])
|
|
756
|
+
]);
|
|
757
|
+
}
|
|
758
|
+
|
|
628
759
|
function packetHandlingMarkdown(packet) {
|
|
629
760
|
return [
|
|
630
761
|
"# Packet Handling",
|
|
@@ -645,7 +776,7 @@ function packetHandlingMarkdown(packet) {
|
|
|
645
776
|
}
|
|
646
777
|
|
|
647
778
|
function evidenceIndexCsv(packet) {
|
|
648
|
-
const v4 = packet.dataModelVersion
|
|
779
|
+
const v4 = modelSupports(packet.dataModelVersion, "component-sources");
|
|
649
780
|
return csv([
|
|
650
781
|
["Evidence ID", v4 ? "Evidence Artifact" : "Evidence", "Status", "Kind", "Source", v4 ? "Source Component ID" : "Source System ID", v4 ? "Source Component" : "Source System", "Collected On", "Collector IDs", "Verified On", "Verifier IDs", "Period Start", "Period End", "Generated At", "Timezone", "Query or Report Parameters", "Population Count", "Completeness Validation", "Accuracy Validation", "Control IDs", "Source Resource IDs", "Source Commit", "File Paths", "External Reference"],
|
|
651
782
|
...packet.evidence.map((item) => [
|
|
@@ -678,7 +809,7 @@ function evidenceIndexCsv(packet) {
|
|
|
678
809
|
}
|
|
679
810
|
|
|
680
811
|
function sourceSystemIndexCsv(packet) {
|
|
681
|
-
const v4 = packet.dataModelVersion
|
|
812
|
+
const v4 = modelSupports(packet.dataModelVersion, "component-sources");
|
|
682
813
|
return csv([
|
|
683
814
|
[v4 ? "Component ID" : "System ID", v4 ? "Component" : "System", "Status", "Evidence Source Roles", "Evidence Access Owner IDs", "Vendor ID", v4 ? "Supports Audit System" : "In Audit Scope", "Evidence IDs"],
|
|
684
815
|
...(packet.sourceComponents || packet.sourceSystems || []).map((item) => [
|
|
@@ -695,7 +826,7 @@ function sourceSystemIndexCsv(packet) {
|
|
|
695
826
|
}
|
|
696
827
|
|
|
697
828
|
function externalEvidenceIndexCsv(packet) {
|
|
698
|
-
const v4 = packet.dataModelVersion
|
|
829
|
+
const v4 = modelSupports(packet.dataModelVersion, "component-sources");
|
|
699
830
|
return csv([
|
|
700
831
|
["Evidence ID", v4 ? "Evidence Artifact" : "Evidence", v4 ? "Source Component ID" : "Source System ID", v4 ? "Source Component" : "Source System", "Control IDs", "External Reference", "Fixed Attachment Included", "Delivery Note"],
|
|
701
832
|
...packet.evidence
|
|
@@ -716,7 +847,7 @@ function externalEvidenceIndexCsv(packet) {
|
|
|
716
847
|
}
|
|
717
848
|
|
|
718
849
|
function populationIndexCsv(packet) {
|
|
719
|
-
const v4 = packet.dataModelVersion
|
|
850
|
+
const v4 = modelSupports(packet.dataModelVersion, "component-sources");
|
|
720
851
|
return csv([
|
|
721
852
|
["Population ID", "Population", "Kind", "Status", "Period Start", "Period End", v4 ? "Source Component ID" : "Source System ID", v4 ? "Source Component" : "Source System", "Authoritative Source", "Query or Report Parameters", "Timezone", "Generated At", "Record Count", "Completeness Validation", "Accuracy Validation", "Reconciled By", "Reconciled On", "Conclusion", "Control IDs", "Evidence ID", "Not Applicable Reason"],
|
|
722
853
|
...packet.populations.map((item) => [
|
|
@@ -769,7 +900,7 @@ function requireDerivedOutputPath(value) {
|
|
|
769
900
|
async function assertPacketSourceState(packet, loaded) {
|
|
770
901
|
await assertLoadedEntriesCurrent(loaded);
|
|
771
902
|
const dataDigest = await dataTreeDigest(loaded.root);
|
|
772
|
-
const git =
|
|
903
|
+
const git = await getWorkspaceRevisionSnapshot(loaded.root);
|
|
773
904
|
if (
|
|
774
905
|
packet.revision?.dataDigest !== dataDigest
|
|
775
906
|
|| packet.revision?.commit !== git.commit
|
|
@@ -789,36 +920,38 @@ async function assertLoadedEntriesCurrent(loaded) {
|
|
|
789
920
|
}
|
|
790
921
|
|
|
791
922
|
async function dataTreeDigest(root) {
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
923
|
+
return measureTiming("packet-data-hash", async () => {
|
|
924
|
+
const hash = createHash("sha256");
|
|
925
|
+
updateDigestField(hash, "filegrc-data-tree-v1");
|
|
926
|
+
const visit = async (directory, prefix = "") => {
|
|
927
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
928
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
929
|
+
for (const entry of entries) {
|
|
930
|
+
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
931
|
+
const path = join(directory, entry.name);
|
|
932
|
+
if (entry.isDirectory()) {
|
|
933
|
+
updateDigestField(hash, "directory");
|
|
934
|
+
updateDigestField(hash, relativePath);
|
|
935
|
+
await visit(path, relativePath);
|
|
936
|
+
} else if (entry.isFile()) {
|
|
937
|
+
const fileHash = createHash("sha256");
|
|
938
|
+
for await (const chunk of createReadStream(path)) fileHash.update(chunk);
|
|
939
|
+
updateDigestField(hash, "file");
|
|
940
|
+
updateDigestField(hash, relativePath);
|
|
941
|
+
updateDigestField(hash, fileHash.digest("hex"));
|
|
942
|
+
} else if (entry.isSymbolicLink()) {
|
|
943
|
+
updateDigestField(hash, "symlink");
|
|
944
|
+
updateDigestField(hash, relativePath);
|
|
945
|
+
updateDigestField(hash, await readlink(path));
|
|
946
|
+
} else {
|
|
947
|
+
updateDigestField(hash, "other");
|
|
948
|
+
updateDigestField(hash, relativePath);
|
|
949
|
+
}
|
|
817
950
|
}
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
951
|
+
};
|
|
952
|
+
await visit(resolveDataPath(root, "."));
|
|
953
|
+
return `sha256:${hash.digest("hex")}`;
|
|
954
|
+
});
|
|
822
955
|
}
|
|
823
956
|
|
|
824
957
|
function updateDigestField(hash, value) {
|
|
@@ -993,7 +1126,7 @@ function packetGaps({
|
|
|
993
1126
|
if (!audit) {
|
|
994
1127
|
gaps.push(gap("error", "missing-audit-scope", "Select an audit record before treating this packet as an auditor delivery."));
|
|
995
1128
|
} else {
|
|
996
|
-
auditGaps(gaps, audit, byId, records, start, end);
|
|
1129
|
+
auditGaps(gaps, audit, byId, records, start, end, model);
|
|
997
1130
|
}
|
|
998
1131
|
for (const stage of managementPreparation?.stages || []) {
|
|
999
1132
|
for (const item of stage.items.filter((entry) => ["action", "later"].includes(entry.status))) {
|
|
@@ -1031,7 +1164,7 @@ function packetGaps({
|
|
|
1031
1164
|
}
|
|
1032
1165
|
}
|
|
1033
1166
|
if (controlNeedsExternalEvidence(control, model) && !coverage.evidenceIds.length) {
|
|
1034
|
-
gaps.push(gap("error", "control-missing-external-evidence", `${coverage.code || coverage.title} relies on an external source but has no linked ${
|
|
1167
|
+
gaps.push(gap("error", "control-missing-external-evidence", `${coverage.code || coverage.title} relies on an external source but has no linked ${modelSupports(model, "evidence-artifacts") ? "Evidence Artifact" : "External Evidence"} in the packet.`, coverage.id));
|
|
1035
1168
|
} else if (!controlNeedsExternalEvidence(control, model) && !coverage.operatingRecordIds.length) {
|
|
1036
1169
|
gaps.push(gap("error", "control-missing-filegrc-evidence", `${coverage.code || coverage.title} has no dated filegrc operating record in the packet.`, coverage.id));
|
|
1037
1170
|
}
|
|
@@ -1089,7 +1222,7 @@ function packetGaps({
|
|
|
1089
1222
|
|
|
1090
1223
|
for (const requirementId of requirementIds) {
|
|
1091
1224
|
const requirement = byId.get(requirementId);
|
|
1092
|
-
if (!requirement || requirement
|
|
1225
|
+
if (!requirement || !requirementIsApplicable(requirement, audit, byId, model) || isDescriptionRequirement(requirement)) continue;
|
|
1093
1226
|
const mapped = controlCoverage.some((coverage) => coverage.requirementIds.includes(requirementId));
|
|
1094
1227
|
if (!mapped) gaps.push(gap("error", "requirement-missing-control", `${requirement.reference || requirement.title} has no control in the packet.`, requirementId));
|
|
1095
1228
|
}
|
|
@@ -1120,11 +1253,11 @@ function packetGaps({
|
|
|
1120
1253
|
if (!recentAssessment) {
|
|
1121
1254
|
gaps.push(gap("error", "missing-risk-assessment", `No completed in-scope risk assessment was found in the year ending ${end}.`, audit.id));
|
|
1122
1255
|
}
|
|
1123
|
-
for (const vendorId of audit
|
|
1256
|
+
for (const vendorId of auditSubserviceVendorIds(audit)) {
|
|
1124
1257
|
const vendor = byId.get(vendorId);
|
|
1125
1258
|
if (!vendor) continue;
|
|
1126
|
-
if (vendor
|
|
1127
|
-
gaps.push(gap("error", "inactive-subservice-organization", `${vendor.title}
|
|
1259
|
+
if (!recordWasInUseDuringAudit(vendor, start, end)) {
|
|
1260
|
+
gaps.push(gap("error", "inactive-subservice-organization", `${vendor.title} was not in use during the engagement period.`, vendor.id));
|
|
1128
1261
|
}
|
|
1129
1262
|
const reviews = records.filter((record) => (
|
|
1130
1263
|
record.type === "vendor-review"
|
|
@@ -1247,7 +1380,7 @@ function packetGaps({
|
|
|
1247
1380
|
}
|
|
1248
1381
|
}
|
|
1249
1382
|
} else if (["population-export", "system-export", "configuration-export"].includes(item.artifactKind)) {
|
|
1250
|
-
gaps.push(gap("error", "evidence-source-system-unrecorded", `${item.title} is a source export but does not link the cataloged ${
|
|
1383
|
+
gaps.push(gap("error", "evidence-source-system-unrecorded", `${item.title} is a source export but does not link the cataloged ${modelSupports(model, "component-sources") ? "source Component" : "system of record"}.`, item.id));
|
|
1251
1384
|
}
|
|
1252
1385
|
if (item.externalReference && !item.filePaths.length) {
|
|
1253
1386
|
gaps.push(gap("warning", "external-only-evidence", `${item.title} relies on an external reference and is not self-contained in the packet.`, item.id));
|
|
@@ -1302,7 +1435,11 @@ function controlNeedsExternalEvidence(control, model) {
|
|
|
1302
1435
|
return !families.length || families.some((family) => family.filegrcManaged !== true);
|
|
1303
1436
|
}
|
|
1304
1437
|
|
|
1305
|
-
function auditGaps(gaps, audit, byId, records, start, end) {
|
|
1438
|
+
function auditGaps(gaps, audit, byId, records, start, end, model) {
|
|
1439
|
+
const program = byId.get(audit.programId);
|
|
1440
|
+
if (modelSupports(model, "program-scope") && program?.assuranceGoal !== audit.auditKind) {
|
|
1441
|
+
gaps.push(gap("error", "audit-program-goal-mismatch", `${audit.title} is ${audit.auditKind}, but its Program goal is ${program?.assuranceGoal || "missing"}. Align the management objective with the formal engagement before delivery.`, audit.id));
|
|
1442
|
+
}
|
|
1306
1443
|
if (!["soc-2-type-1", "soc-2-type-2"].includes(audit.auditKind)) {
|
|
1307
1444
|
gaps.push(gap("error", "not-soc2-examination", `${audit.title} is ${audit.auditKind}; a delivery packet requires a SOC 2 Type 1 or Type 2 engagement.`, audit.id));
|
|
1308
1445
|
} else if (audit.auditKind === "soc-2-type-1") {
|
|
@@ -1321,34 +1458,118 @@ function auditGaps(gaps, audit, byId, records, start, end) {
|
|
|
1321
1458
|
if (!(audit.requirementIds || []).length) gaps.push(gap("error", "audit-requirements-missing", `${audit.title} has no selected criteria.`, audit.id));
|
|
1322
1459
|
if (!(audit.controlIds || []).length) gaps.push(gap("error", "audit-controls-missing", `${audit.title} has no selected controls.`, audit.id));
|
|
1323
1460
|
const selectedRequirements = (audit.requirementIds || []).map((id) => byId.get(id)).filter(Boolean);
|
|
1324
|
-
|
|
1461
|
+
const frameworkRequirements = records.filter((record) => (
|
|
1462
|
+
record.type === "requirement" && (audit.frameworkIds || []).includes(record.frameworkId)
|
|
1463
|
+
));
|
|
1464
|
+
const descriptionRequirements = frameworkRequirements.filter(isDescriptionRequirement);
|
|
1465
|
+
const missingDescriptionRequirements = descriptionRequirements.filter((requirement) => (
|
|
1466
|
+
!(audit.requirementIds || []).includes(requirement.id)
|
|
1467
|
+
));
|
|
1468
|
+
const missingRequiredDescriptionReferences = modelSupports(model, "program-scope")
|
|
1469
|
+
? missingSoc2References(descriptionRequirements, REQUIRED_SOC2_DESCRIPTION_REFERENCES)
|
|
1470
|
+
: [];
|
|
1471
|
+
const securityRequirements = frameworkRequirements.filter(isSecurityRequirement);
|
|
1472
|
+
const missingRequiredSecurityReferences = modelSupports(model, "program-scope")
|
|
1473
|
+
? missingSoc2References(securityRequirements, REQUIRED_SOC2_SECURITY_REFERENCES)
|
|
1474
|
+
: [];
|
|
1475
|
+
const missingSelectedSecurityReferences = modelSupports(model, "program-scope")
|
|
1476
|
+
? missingSoc2References(selectedRequirements.filter(isSecurityRequirement), REQUIRED_SOC2_SECURITY_REFERENCES)
|
|
1477
|
+
: [];
|
|
1478
|
+
if (!descriptionRequirements.length) {
|
|
1325
1479
|
gaps.push(gap("error", "audit-description-criteria-missing", `${audit.title} does not include the SOC 2 description criteria.`, audit.id));
|
|
1480
|
+
} else if (missingDescriptionRequirements.length) {
|
|
1481
|
+
gaps.push(gap("error", "audit-description-criteria-incomplete", `${audit.title} omits ${missingDescriptionRequirements.length} ${missingDescriptionRequirements.length === 1 ? "criterion" : "criteria"} from the selected SOC 2 Description Criteria framework.`, audit.id));
|
|
1482
|
+
} else if (missingRequiredDescriptionReferences.length) {
|
|
1483
|
+
gaps.push(gap("error", "audit-description-criteria-incomplete", `${audit.title} omits ${missingRequiredDescriptionReferences.join(", ")} from the required DC1 through DC9 Description Criteria set.`, audit.id));
|
|
1484
|
+
}
|
|
1485
|
+
if (missingRequiredSecurityReferences.length) {
|
|
1486
|
+
gaps.push(gap("error", "audit-security-criteria-incomplete", `${audit.title}'s selected framework omits ${missingRequiredSecurityReferences.join(", ")} from the required CC1.1 through CC9.2 Security Common Criteria set.`, audit.id));
|
|
1487
|
+
}
|
|
1488
|
+
if (missingSelectedSecurityReferences.length) {
|
|
1489
|
+
gaps.push(gap("error", "audit-security-criteria-incomplete", `${audit.title} omits ${missingSelectedSecurityReferences.join(", ")} from the mandatory Security Common Criteria selected for the engagement.`, audit.id));
|
|
1326
1490
|
}
|
|
1327
1491
|
for (const requirement of selectedRequirements) {
|
|
1328
|
-
if (!(audit.frameworkIds || []).includes(requirement.frameworkId) || requirement
|
|
1492
|
+
if (!(audit.frameworkIds || []).includes(requirement.frameworkId) || !requirementIsApplicable(requirement, audit, byId, model)) {
|
|
1329
1493
|
gaps.push(gap("error", "audit-criteria-scope-conflict", `${requirement.reference || requirement.title} is selected but is not an applicable member of the selected frameworks.`, requirement.id));
|
|
1330
1494
|
}
|
|
1331
1495
|
}
|
|
1332
|
-
|
|
1333
|
-
if (!
|
|
1334
|
-
|
|
1335
|
-
|
|
1496
|
+
const auditor = audit.auditorVendorId ? byId.get(audit.auditorVendorId) : null;
|
|
1497
|
+
if (!auditor) {
|
|
1498
|
+
gaps.push(gap("error", "auditor-missing", `${audit.title} does not identify the independent CPA firm.`, audit.id));
|
|
1499
|
+
} else if (!auditorWasEngaged(auditor, audit)) {
|
|
1500
|
+
gaps.push(gap("error", "auditor-outside-engagement-period", `${auditor.title} was not active during the recorded fieldwork or report period.`, auditor.id));
|
|
1336
1501
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
if (!(audit.subserviceVendorIds || []).includes(vendorId)) {
|
|
1340
|
-
gaps.push(gap("error", "subservice-organization-omitted", `${byId.get(vendorId)?.title || vendorId} is identified by an in-scope system but omitted from the engagement's subservice organizations.`, vendorId));
|
|
1341
|
-
}
|
|
1502
|
+
if (modelSupports(model, "program-scope") && !audit.scopeRevision) {
|
|
1503
|
+
gaps.push(gap("error", "audit-scope-revision-missing", `${audit.title} does not bind management's reviewed engagement scope to a Git revision.`, audit.id));
|
|
1342
1504
|
}
|
|
1343
|
-
if (
|
|
1344
|
-
const
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1505
|
+
if (modelSupports(model, "program-scope")) {
|
|
1506
|
+
const treatments = audit.subserviceTreatments || [];
|
|
1507
|
+
const treatmentComponentCounts = new Map();
|
|
1508
|
+
for (const treatment of treatments) {
|
|
1509
|
+
for (const componentId of treatment.componentIds || []) {
|
|
1510
|
+
treatmentComponentCounts.set(componentId, (treatmentComponentCounts.get(componentId) || 0) + 1);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
if (!audit.subserviceConclusion) {
|
|
1514
|
+
gaps.push(gap("error", "subservice-conclusion-missing", `${audit.title} does not state whether subservice organizations are identified.`, audit.id));
|
|
1515
|
+
}
|
|
1516
|
+
if (!audit.subserviceConclusionRationale) {
|
|
1517
|
+
gaps.push(gap("error", "subservice-rationale-missing", `${audit.title} does not explain its subservice conclusion.`, audit.id));
|
|
1518
|
+
}
|
|
1519
|
+
if (audit.subserviceConclusion === "identified" && !treatments.length) {
|
|
1520
|
+
gaps.push(gap("error", "subservice-treatments-missing", `${audit.title} identifies subservice organizations but records no Vendor and Component treatments.`, audit.id));
|
|
1521
|
+
}
|
|
1522
|
+
if (audit.subserviceConclusion === "not-applicable" && treatments.length) {
|
|
1523
|
+
gaps.push(gap("error", "subservice-scope-conflict", `${audit.title} records subservice treatments but marks subservice organizations not applicable.`, audit.id));
|
|
1524
|
+
}
|
|
1525
|
+
for (const treatment of treatments) {
|
|
1526
|
+
const vendor = byId.get(treatment.vendorId);
|
|
1527
|
+
const invalidComponents = (treatment.componentIds || []).filter((componentId) => {
|
|
1528
|
+
const component = byId.get(componentId);
|
|
1529
|
+
return component?.type !== "component"
|
|
1530
|
+
|| !recordWasInUseDuringAudit(component, start, end)
|
|
1531
|
+
|| component.vendorId !== treatment.vendorId
|
|
1532
|
+
|| (treatmentComponentCounts.get(componentId) || 0) > 1
|
|
1533
|
+
|| !(component.systemUses || []).some(({ systemId }) => (audit.systemIds || []).includes(systemId));
|
|
1534
|
+
});
|
|
1535
|
+
if (vendor?.type !== "vendor" || !recordWasInUseDuringAudit(vendor, start, end) || !(treatment.componentIds || []).length || invalidComponents.length) {
|
|
1536
|
+
gaps.push(gap(
|
|
1537
|
+
"error",
|
|
1538
|
+
"invalid-subservice-treatment",
|
|
1539
|
+
`${vendor?.title || treatment.vendorId} has a subservice treatment that does not identify that Vendor's supplied Components in use within the selected System boundary during the engagement, or repeats a Component across treatments.`,
|
|
1540
|
+
audit.id
|
|
1541
|
+
));
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
for (const treatment of treatments.filter(({ method }) => method === "inclusive")) {
|
|
1545
|
+
const includedControls = (audit.controlIds || [])
|
|
1546
|
+
.map((id) => byId.get(id))
|
|
1547
|
+
.filter((control) => (control?.componentIds || []).some((id) => (treatment.componentIds || []).includes(id)));
|
|
1548
|
+
if (!includedControls.length) {
|
|
1549
|
+
gaps.push(gap("error", "inclusive-subservice-controls-missing", `${byId.get(treatment.vendorId)?.title || treatment.vendorId} uses the inclusive method but no selected Controls are linked to its included Components.`, audit.id));
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
} else {
|
|
1553
|
+
if (!audit.subserviceMethod) gaps.push(gap("error", "subservice-method-missing", `${audit.title} does not state whether subservice organizations use the carve-out or inclusive method, or are not applicable.`, audit.id));
|
|
1554
|
+
if ((audit.subserviceVendorIds || []).length && audit.subserviceMethod === "not-applicable") {
|
|
1555
|
+
gaps.push(gap("error", "subservice-scope-conflict", `${audit.title} names subservice organizations but marks their treatment not applicable.`, audit.id));
|
|
1556
|
+
}
|
|
1557
|
+
const expectedSubserviceVendorIds = new Set((audit.systemIds || []).flatMap((id) => byId.get(id)?.subserviceVendorIds || []));
|
|
1558
|
+
for (const vendorId of expectedSubserviceVendorIds) {
|
|
1559
|
+
if (!(audit.subserviceVendorIds || []).includes(vendorId)) {
|
|
1560
|
+
gaps.push(gap("error", "subservice-organization-omitted", `${byId.get(vendorId)?.title || vendorId} is identified by an in-scope system but omitted from the engagement's subservice organizations.`, vendorId));
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
if (audit.subserviceMethod === "inclusive") {
|
|
1564
|
+
const subserviceSystemIds = records
|
|
1565
|
+
.filter((record) => record.type === "system" && (audit.subserviceVendorIds || []).includes(record.vendorId))
|
|
1566
|
+
.map((record) => record.id);
|
|
1567
|
+
const includedControls = (audit.controlIds || [])
|
|
1568
|
+
.map((id) => byId.get(id))
|
|
1569
|
+
.filter((control) => (control?.systemIds || []).some((id) => subserviceSystemIds.includes(id)));
|
|
1570
|
+
if (!subserviceSystemIds.length || !includedControls.length) {
|
|
1571
|
+
gaps.push(gap("error", "inclusive-subservice-controls-missing", `${audit.title} uses the inclusive method but does not include cataloged subservice systems and their controls.`, audit.id));
|
|
1572
|
+
}
|
|
1352
1573
|
}
|
|
1353
1574
|
}
|
|
1354
1575
|
if (!audit.complementaryControlsConclusion) {
|
|
@@ -1385,15 +1606,42 @@ function auditGaps(gaps, audit, byId, records, start, end) {
|
|
|
1385
1606
|
gaps.push(gap("error", `unapproved-${field}`, `${document.title} is not active with approval and effective dates.`, document.id));
|
|
1386
1607
|
}
|
|
1387
1608
|
}
|
|
1388
|
-
if (
|
|
1389
|
-
const
|
|
1390
|
-
if (!
|
|
1391
|
-
gaps.push(gap("error", "
|
|
1609
|
+
if (["issued", "delivered", "complete"].includes(audit.status)) {
|
|
1610
|
+
const reportEvidence = audit.reportEvidenceId ? byId.get(audit.reportEvidenceId) : null;
|
|
1611
|
+
if (!reportEvidence) {
|
|
1612
|
+
gaps.push(gap("error", "missing-audit-report", `${audit.title} does not link the final service auditor report.`, audit.id));
|
|
1613
|
+
} else {
|
|
1614
|
+
const reportIssue = soc2ReportEvidenceIssue(reportEvidence, audit, model.modelVersion);
|
|
1615
|
+
if (reportIssue) gaps.push(gap("error", reportIssue.code, reportIssue.message, reportEvidence.id));
|
|
1392
1616
|
}
|
|
1393
|
-
if (!audit.reportEvidenceId) gaps.push(gap("error", "missing-audit-report", `${audit.title} does not link the final service auditor report.`, audit.id));
|
|
1394
1617
|
if (!audit.opinion || audit.opinion === "not-issued" || !audit.opinionDate) {
|
|
1395
1618
|
gaps.push(gap("error", "missing-audit-opinion", `${audit.title} does not record the issued opinion and opinion date.`, audit.id));
|
|
1396
1619
|
}
|
|
1620
|
+
}
|
|
1621
|
+
if (modelSupports(model, "program-scope") && ["report-draft", "issued", "delivered", "complete"].includes(audit.status)) {
|
|
1622
|
+
const subsequentEventsIssue = subsequentEventsReviewIssue(audit);
|
|
1623
|
+
if (subsequentEventsIssue) {
|
|
1624
|
+
gaps.push(gap("error", subsequentEventsIssue.code, subsequentEventsIssue.message, audit.id));
|
|
1625
|
+
}
|
|
1626
|
+
const signatoryIssue = signatoryAppointmentIssue(audit, byId);
|
|
1627
|
+
if (signatoryIssue) {
|
|
1628
|
+
gaps.push(gap("error", signatoryIssue.code, signatoryIssue.message, audit.id));
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
if (audit.status === "complete") {
|
|
1632
|
+
const representation = byId.get(audit.managementRepresentationDocumentId);
|
|
1633
|
+
const signedRepresentation = (representation?.evidenceIds || [])
|
|
1634
|
+
.map((id) => byId.get(id))
|
|
1635
|
+
.find((record) => (
|
|
1636
|
+
record?.type === "evidence"
|
|
1637
|
+
&& record.status === "verified"
|
|
1638
|
+
&& record.artifactKind === "signed-record"
|
|
1639
|
+
&& record.artifactSubtype === "signed-management-representation"
|
|
1640
|
+
&& (record.filePaths || []).length
|
|
1641
|
+
));
|
|
1642
|
+
if (!signedRepresentation) {
|
|
1643
|
+
gaps.push(gap("error", "unsigned-management-representation", `${representation?.title || audit.title} does not link the signed representation letter evidence.`, representation?.id || audit.id));
|
|
1644
|
+
}
|
|
1397
1645
|
for (const request of records.filter((record) => record.type === "audit-request" && record.auditId === audit.id)) {
|
|
1398
1646
|
if (!["accepted", "closed"].includes(request.status)) {
|
|
1399
1647
|
gaps.push(gap("error", "open-final-audit-request", `${request.title} is ${request.status} after the audit was marked complete.`, request.id));
|
|
@@ -1418,6 +1666,39 @@ function isDescriptionRequirement(requirement) {
|
|
|
1418
1666
|
return (requirement.tags || []).includes("description-criteria") || /^DC\d+/i.test(requirement.reference || "");
|
|
1419
1667
|
}
|
|
1420
1668
|
|
|
1669
|
+
function isSecurityRequirement(requirement) {
|
|
1670
|
+
const tags = requirement?.tags || [];
|
|
1671
|
+
return tags.includes("security") || tags.includes("common-criteria") || /^CC\d+(?:\.|$)/i.test(requirement?.reference || "");
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
function requirementIsApplicable(requirement, audit, byId, model) {
|
|
1675
|
+
if (!modelSupports(model, "program-scope")) return requirement.applicability === "applicable";
|
|
1676
|
+
const program = audit?.programId ? byId.get(audit.programId) : null;
|
|
1677
|
+
return (program?.requirementApplicability || []).some((decision) => (
|
|
1678
|
+
decision.requirementId === requirement.id && decision.decision === "applicable"
|
|
1679
|
+
));
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
function auditSubserviceVendorIds(audit) {
|
|
1683
|
+
return [...new Set([
|
|
1684
|
+
...(audit?.subserviceVendorIds || []),
|
|
1685
|
+
...(audit?.subserviceTreatments || []).map(({ vendorId }) => vendorId)
|
|
1686
|
+
].filter(Boolean))];
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
function auditSubserviceComponentIds(audit) {
|
|
1690
|
+
return [...new Set((audit?.subserviceTreatments || []).flatMap(({ componentIds }) => componentIds || []))];
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
function auditSubserviceLabel(audit) {
|
|
1694
|
+
if (audit?.subserviceConclusion === "not-applicable") return "Not applicable";
|
|
1695
|
+
if (audit?.subserviceConclusion === "identified") {
|
|
1696
|
+
const methods = [...new Set((audit.subserviceTreatments || []).map(({ method }) => method))];
|
|
1697
|
+
return methods.length ? methods.join(" and ") : "Identified, treatments incomplete";
|
|
1698
|
+
}
|
|
1699
|
+
return audit?.subserviceMethod || null;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1421
1702
|
function shiftYear(value, offset) {
|
|
1422
1703
|
const date = new Date(`${value}T00:00:00Z`);
|
|
1423
1704
|
date.setUTCFullYear(date.getUTCFullYear() + offset);
|
|
@@ -1619,7 +1900,7 @@ function recordSummary(record) {
|
|
|
1619
1900
|
}
|
|
1620
1901
|
|
|
1621
1902
|
function packetMarkdown(packet) {
|
|
1622
|
-
const v4 = packet.dataModelVersion
|
|
1903
|
+
const v4 = modelSupports(packet.dataModelVersion, "component-sources");
|
|
1623
1904
|
const readiness = packet.readiness.status === "delivery-ready"
|
|
1624
1905
|
? "filegrc management checks passed. The engagement team still determines whether the evidence is sufficient and appropriate."
|
|
1625
1906
|
: `${packet.readiness.errors} errors and ${packet.readiness.warnings} warnings require review. This is a draft packet.`;
|
|
@@ -1646,6 +1927,9 @@ function packetMarkdown(packet) {
|
|
|
1646
1927
|
`- ${packet.summary.eventRuns} event runs`,
|
|
1647
1928
|
`- ${packet.summary.evidence} ${v4 ? "Evidence Artifact" : "External Evidence"} records`,
|
|
1648
1929
|
`- ${packet.summary.populations} reconciled or planned populations`,
|
|
1930
|
+
...(modelSupports(packet.dataModelVersion, "governed-document-activation")
|
|
1931
|
+
? [`- ${packet.summary.documents} governed engagement Documents with approval and activation history`]
|
|
1932
|
+
: []),
|
|
1649
1933
|
`- ${packet.summary.policies} policies`,
|
|
1650
1934
|
`- ${packet.summary.controls} controls`,
|
|
1651
1935
|
`- ${packet.summary.requirements} criteria`,
|
|
@@ -1653,7 +1937,7 @@ function packetMarkdown(packet) {
|
|
|
1653
1937
|
`- ${v4 ? packet.summary.sourceComponents : packet.summary.sourceSystems} cataloged source ${v4 ? "Components" : "Systems"}`,
|
|
1654
1938
|
"",
|
|
1655
1939
|
v4
|
|
1656
|
-
?
|
|
1940
|
+
? `Open \`index.html\` for the auditor-oriented index. \`control-matrix.csv\` cross-references criteria, Controls, filegrc Evidence, Evidence Artifacts, and tests.${modelSupports(packet.dataModelVersion, "governed-document-activation") ? " `document-lifecycle-index.csv` lists each engagement Document's approval and activation facts and exact Markdown revisions." : ""} \`source-component-index.csv\` identifies the Components used to produce Evidence. \`evidence-artifact-index.csv\` lists material that must be delivered or accessed outside this packet. For Type 2, \`population-index.csv\` records management's population reconciliation and fixed source exports. FileGRC records, governed Markdown, fixed attachments, and committed historical versions are included in their respective directories.`
|
|
1657
1941
|
: "Open `index.html` for the auditor-oriented index. `control-matrix.csv` cross-references criteria, controls, filegrc Evidence, External Evidence, and tests. `source-system-index.csv` identifies the systems of record used to produce External Evidence. `external-evidence-index.csv` lists material that must be delivered or accessed outside this packet. For Type 2, `population-index.csv` records management's population reconciliation and fixed source exports. filegrc records, governed Markdown, fixed attachments, and committed historical versions are included in their respective directories.",
|
|
1658
1942
|
"",
|
|
1659
1943
|
"After transfer, enter the packet directory and run `shasum -a 256 -c SHA256SUMS` or `sha256sum -c SHA256SUMS`. The checksum file covers every other packet file.",
|
|
@@ -1663,7 +1947,7 @@ function packetMarkdown(packet) {
|
|
|
1663
1947
|
}
|
|
1664
1948
|
|
|
1665
1949
|
function packetHtml(packet) {
|
|
1666
|
-
const v4 = packet.dataModelVersion
|
|
1950
|
+
const v4 = modelSupports(packet.dataModelVersion, "component-sources");
|
|
1667
1951
|
const artifactLabel = v4 ? "Evidence Artifact" : "External Evidence";
|
|
1668
1952
|
const section = (title, body) => `<section><h2>${escapeHtml(title)}</h2>${body}</section>`;
|
|
1669
1953
|
const links = (items) => items.length
|
|
@@ -1709,13 +1993,16 @@ function packetHtml(packet) {
|
|
|
1709
1993
|
const controlCoverage = packet.controlCoverage.length
|
|
1710
1994
|
? `<p><a href="control-matrix.csv">Download control matrix CSV</a></p><table><thead><tr><th>Control</th><th>Status and scope</th><th>Criteria</th><th>filegrc Evidence</th><th>${artifactLabel}</th><th>Tests</th></tr></thead><tbody>${packet.controlCoverage.map((control) => `<tr><td><a href="records/control/${encodeURIComponent(control.id)}.json">${escapeHtml(control.code || control.id)}</a><small>${escapeHtml(control.title)}</small></td><td>${escapeHtml(control.status)}<small>${control.systemIds.map(escapeHtml).join(", ") || "No system scope"}</small></td><td>${control.requirementIds.map(escapeHtml).join("<br>") || "None"}</td><td>${control.operatingRecordIds.length}</td><td>${control.evidenceIds.length}</td><td>${control.tests.length}</td></tr>`).join("")}</tbody></table>`
|
|
1711
1995
|
: "<p>No controls were selected.</p>";
|
|
1996
|
+
const documentLifecycles = packet.documentLifecycles?.length
|
|
1997
|
+
? `<p><a href="document-lifecycle-index.csv">Download Document lifecycle index CSV</a></p><table><thead><tr><th>Document</th><th>Audit role</th><th>Approval</th><th>Activation</th></tr></thead><tbody>${packet.documentLifecycles.map((document) => `<tr><td><a href="records/document/${encodeURIComponent(document.id)}.json">${escapeHtml(document.title)}</a><small>${escapeHtml(document.status)} · ${escapeHtml(document.workflowScope || "scope not recorded")}</small></td><td>${document.roles.map(escapeHtml).join("<br>")}</td><td>${escapeHtml(document.approvedOn || "Not approved")}<small>${document.approvers.map(escapeHtml).join(", ") || "No approver recorded"}</small></td><td>${escapeHtml(document.activatedOn || "Not activated")}<small>${document.activators.map(escapeHtml).join(", ") || "No activator recorded"}${document.effectiveOn ? ` · effective ${escapeHtml(document.effectiveOn)}` : ""}</small></td></tr>`).join("")}</tbody></table>`
|
|
1998
|
+
: "<p>No governed engagement Documents were linked to this audit.</p>";
|
|
1712
1999
|
const readinessLabel = packet.readiness.status === "delivery-ready" ? "filegrc management checks passed" : "Draft, do not deliver";
|
|
1713
2000
|
const packetDate = packet.period.basis === "as-of"
|
|
1714
2001
|
? `As of ${escapeHtml(packet.period.start)}`
|
|
1715
2002
|
: `${escapeHtml(packet.period.start)} through ${escapeHtml(packet.period.end)}`;
|
|
1716
2003
|
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Evidence packet</title><style>
|
|
1717
2004
|
body{font:14px/1.5 system-ui,sans-serif;color:#161825;max-width:1120px;margin:auto;padding:40px;background:#f7f8fc}header,section{background:#fff;border:1px solid #dfe3ef;border-radius:10px;padding:24px;margin:14px 0}h1,h2{margin-top:0}h1{font-size:26px}h2{font-size:17px}ul{padding-left:20px}li{margin:8px 0}small{display:block;color:#656c7e}.attachment{margin-right:10px;font-size:12px}.error{color:#8a2f28}.warning{color:#76500d}.readiness{display:inline-block;padding:5px 9px;border-radius:999px;background:#f7e4e2;color:#7a2520;font-weight:700}.readiness.ready{background:#e2f1e8;color:#245d3b}table{width:100%;border-collapse:collapse}th,td{padding:9px;border:1px solid #dfe3ef;text-align:left;vertical-align:top}code{overflow-wrap:anywhere}dl{display:grid;grid-template-columns:max-content 1fr;gap:8px 16px}dt{font-weight:700}dd{margin:0}
|
|
1718
|
-
</style></head><body><header><p>SOC 2 evidence packet</p><span class="readiness ${packet.readiness.status === "delivery-ready" ? "ready" : ""}">${escapeHtml(readinessLabel)}</span><h1>${packetDate}</h1><p>${escapeHtml(packet.workspace.organizationName)} · revision <code>${escapeHtml(packet.revision.commit || "uncommitted")}</code></p></header>${section("Engagement scope", engagement)}${section("Review status", gaps)}${section("Control coverage", controlCoverage)}${section(v4 ? "Source Components" : "Systems of record", sourceSystems)}${packet.period.basis === "period" ? section("Management population reconciliation", populations) : ""}${packet.period.basis === "period" ? section("Recurring obligation coverage", obligations) : ""}${packet.period.basis === "period" ? section("Event workflow coverage", eventRuns) : ""}${section("Policies", links(packet.policies))}${section("filegrc Evidence", filegrcRecords)}${section(v4 ? "Evidence Artifacts" : "External Evidence", evidence)}${section("Integrity and history", "<p>Verify all transferred files with <code>SHA256SUMS</code>. Committed prior versions are under <code>history/</code> with an index that records their source paths and Git metadata.</p>")}</body></html>`;
|
|
2005
|
+
</style></head><body><header><p>SOC 2 evidence packet</p><span class="readiness ${packet.readiness.status === "delivery-ready" ? "ready" : ""}">${escapeHtml(readinessLabel)}</span><h1>${packetDate}</h1><p>${escapeHtml(packet.workspace.organizationName)} · revision <code>${escapeHtml(packet.revision.commit || "uncommitted")}</code></p></header>${section("Engagement scope", engagement)}${section("Review status", gaps)}${modelSupports(packet.dataModelVersion, "governed-document-activation") ? section("Document lifecycle", documentLifecycles) : ""}${section("Control coverage", controlCoverage)}${section(v4 ? "Source Components" : "Systems of record", sourceSystems)}${packet.period.basis === "period" ? section("Management population reconciliation", populations) : ""}${packet.period.basis === "period" ? section("Recurring obligation coverage", obligations) : ""}${packet.period.basis === "period" ? section("Event workflow coverage", eventRuns) : ""}${section("Policies", links(packet.policies))}${section("filegrc Evidence", filegrcRecords)}${section(v4 ? "Evidence Artifacts" : "External Evidence", evidence)}${section("Integrity and history", "<p>Verify all transferred files with <code>SHA256SUMS</code>. Committed prior versions are under <code>history/</code> with an index that records their source paths and Git metadata.</p>")}</body></html>`;
|
|
1719
2006
|
}
|
|
1720
2007
|
|
|
1721
2008
|
async function writePacketFile(output, relativePath, source, files) {
|