filegrc 0.5.0 → 0.6.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 +4 -4
- package/model/v4.json +10052 -0
- package/package.json +2 -2
- package/src/agent.js +17 -5
- package/src/audit-preparation.js +17 -13
- package/src/audit-transition.js +7 -4
- package/src/batch-review.js +31 -8
- package/src/cli.js +43 -25
- package/src/collection-review.js +71 -25
- package/src/evidence-packet.js +76 -43
- package/src/external-reviewer.js +4 -4
- package/src/files.js +70 -6
- package/src/git.js +70 -13
- package/src/index.js +1 -0
- package/src/model-migration.js +631 -14
- package/src/obligations.js +14 -7
- package/src/program-path.js +49 -38
- package/src/program-readiness.js +94 -54
- package/src/program.js +52 -0
- package/src/reconciliation.js +2 -2
- package/src/server.js +50 -8
- package/src/setup.js +56 -21
- package/src/source-coverage.js +13 -9
- package/src/startup.js +1 -1
- package/src/state.js +17 -14
- package/src/timing.js +6 -0
- package/src/validate.js +100 -9
- package/src/web.js +709 -151
- package/src/workflow.js +47 -25
package/src/collection-review.js
CHANGED
|
@@ -2,49 +2,58 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { applyResourceBatch } from "./files.js";
|
|
3
3
|
import { getGitSummary } from "./git.js";
|
|
4
4
|
import { loadWorkspace } from "./workspace.js";
|
|
5
|
+
import { programComponents, resolveProgram, selectedRequirementIds } from "./program.js";
|
|
5
6
|
|
|
6
|
-
export function collectionRevision(loaded, resourceType) {
|
|
7
|
+
export function collectionRevision(loaded, resourceType, options = {}) {
|
|
8
|
+
const program = resolveProgram(loaded, options.programId);
|
|
9
|
+
const scopedIds = new Set(scopedCollectionRecords(loaded, resourceType, program).map(({ id }) => id));
|
|
7
10
|
const records = loaded.entries
|
|
8
|
-
.filter(({ record }) => record.type === resourceType)
|
|
11
|
+
.filter(({ record }) => record.type === resourceType && scopedIds.has(record.id))
|
|
9
12
|
.map(({ record, source }) => ({
|
|
10
13
|
id: record.id,
|
|
11
14
|
revision: createHash("sha256").update(source).digest("hex")
|
|
12
15
|
}))
|
|
13
16
|
.sort((left, right) => left.id.localeCompare(right.id));
|
|
14
17
|
const workspaceScope = {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
programId: program?.id ?? null,
|
|
19
|
+
assuranceGoal: program?.assuranceGoal ?? null,
|
|
20
|
+
candidateCoverage: program?.candidateCoverage ?? null,
|
|
21
|
+
systemIds: [...(program?.systemIds || [])].sort(),
|
|
22
|
+
frameworkIds: [...(program?.frameworkIds || [])].sort(),
|
|
23
|
+
requirementIds: [...selectedRequirementIds(program || {}, loaded.model)].sort(),
|
|
24
|
+
controlIds: [...(program?.controlIds || [])].sort()
|
|
21
25
|
};
|
|
22
26
|
return createHash("sha256")
|
|
23
27
|
.update(JSON.stringify({ resourceType, records, workspaceScope }))
|
|
24
28
|
.digest("hex");
|
|
25
29
|
}
|
|
26
30
|
|
|
27
|
-
export function assessCollectionReviews(input) {
|
|
31
|
+
export function assessCollectionReviews(input, options = {}) {
|
|
28
32
|
const loaded = input?.resources && input?.model && input?.entries
|
|
29
33
|
? input
|
|
30
34
|
: null;
|
|
31
35
|
if (!loaded) throw new Error("Collection review assessment requires a loaded workspace.");
|
|
32
36
|
return Object.keys(loaded.model.collectionReviews || {})
|
|
33
|
-
.map((resourceType) => assessCollectionReview(loaded, resourceType));
|
|
37
|
+
.map((resourceType) => assessCollectionReview(loaded, resourceType, options));
|
|
34
38
|
}
|
|
35
39
|
|
|
36
|
-
export function assessCollectionReview(loaded, resourceType) {
|
|
40
|
+
export function assessCollectionReview(loaded, resourceType, options = {}) {
|
|
37
41
|
const configuration = loaded.model.collectionReviews?.[resourceType];
|
|
38
42
|
if (!configuration) return null;
|
|
39
|
-
const
|
|
43
|
+
const program = resolveProgram(loaded, options.programId);
|
|
44
|
+
const records = scopedCollectionRecords(loaded, resourceType, program);
|
|
40
45
|
const reviewEntry = loaded.entries.find(({ record }) => (
|
|
41
46
|
record.type === "collection-review"
|
|
42
47
|
&& record.resourceType === resourceType
|
|
43
48
|
&& record.status !== "retired"
|
|
49
|
+
&& (String(loaded.model.modelVersion) !== "4" || (record.scopeResourceIds || []).includes(program.id))
|
|
44
50
|
));
|
|
45
51
|
const review = reviewEntry?.record || null;
|
|
46
|
-
const currentRevision = collectionRevision(loaded, resourceType);
|
|
52
|
+
const currentRevision = collectionRevision(loaded, resourceType, { programId: program.id });
|
|
47
53
|
const allowedDecisions = configuration.decisions || ["complete"];
|
|
54
|
+
const allowsEmptyCollection = allowedDecisions.some((decision) => (
|
|
55
|
+
decision === "zero-population" || decision === "externally-managed"
|
|
56
|
+
));
|
|
48
57
|
const complete = Boolean(
|
|
49
58
|
review?.status === "active"
|
|
50
59
|
&& allowedDecisions.includes(review.decision)
|
|
@@ -71,14 +80,17 @@ export function assessCollectionReview(loaded, resourceType) {
|
|
|
71
80
|
? `${configuration.title} were reviewed on ${review.reviewedOn}.`
|
|
72
81
|
: stale
|
|
73
82
|
? `${configuration.title} changed after the last confirmation. Review the current records again.`
|
|
83
|
+
: !records.length && !allowsEmptyCollection
|
|
84
|
+
? `Add at least one ${loaded.model.resources[resourceType].title.toLowerCase()} before confirming this collection.`
|
|
74
85
|
: `Review ${configuration.title.toLowerCase()} before this page can be ready.`
|
|
75
86
|
};
|
|
76
87
|
}
|
|
77
88
|
|
|
78
89
|
export async function scaffoldCollectionReview(input = process.cwd(), options = {}) {
|
|
79
90
|
const loaded = await loadWorkspace(input);
|
|
91
|
+
const program = resolveProgram(loaded, options.programId);
|
|
80
92
|
const resourceType = requiredType(loaded, options.resourceType);
|
|
81
|
-
const assessment = assessCollectionReview(loaded, resourceType);
|
|
93
|
+
const assessment = assessCollectionReview(loaded, resourceType, { programId: program.id });
|
|
82
94
|
const allowedDecisions = assessment.configuration.decisions || ["complete"];
|
|
83
95
|
return {
|
|
84
96
|
resourceType,
|
|
@@ -88,28 +100,37 @@ export async function scaffoldCollectionReview(input = process.cwd(), options =
|
|
|
88
100
|
rationale: null,
|
|
89
101
|
reviewedByIds: [],
|
|
90
102
|
reviewedOn: null,
|
|
91
|
-
|
|
103
|
+
...(String(loaded.model.modelVersion) === "4"
|
|
104
|
+
? { authoritativeComponentId: null }
|
|
105
|
+
: { authoritativeSystemId: null })
|
|
92
106
|
};
|
|
93
107
|
}
|
|
94
108
|
|
|
95
109
|
export async function planCollectionReview(input = process.cwd(), options = {}) {
|
|
96
110
|
const loaded = await loadWorkspace(input);
|
|
111
|
+
const program = resolveProgram(loaded, options.programId);
|
|
97
112
|
const resourceType = requiredType(loaded, options.resourceType);
|
|
98
|
-
const assessment = assessCollectionReview(loaded, resourceType);
|
|
113
|
+
const assessment = assessCollectionReview(loaded, resourceType, { programId: program.id });
|
|
99
114
|
const configuration = assessment.configuration;
|
|
100
115
|
const decision = String(options.decision || "").trim();
|
|
101
116
|
const rationale = String(options.rationale || "").trim();
|
|
102
117
|
const reviewedByIds = [...new Set((options.reviewedByIds || []).map(String).filter(Boolean))];
|
|
103
118
|
const reviewedOn = String(options.reviewedOn || "").trim();
|
|
104
119
|
const scopeRevision = String(options.scopeRevision || getGitSummary(loaded.root).commit || "uncommitted").trim();
|
|
105
|
-
const
|
|
120
|
+
const v4 = String(loaded.model.modelVersion) === "4";
|
|
121
|
+
const authoritativeSourceId = String(v4 ? options.authoritativeComponentId : options.authoritativeSystemId || "").trim();
|
|
106
122
|
if (!(configuration.decisions || ["complete"]).includes(decision)) {
|
|
107
123
|
throw new Error(
|
|
108
124
|
`${configuration.title} review must use one of: ${(configuration.decisions || ["complete"]).join(", ")}.`
|
|
109
125
|
);
|
|
110
126
|
}
|
|
111
127
|
if (!assessment.records.length && decision === "complete") {
|
|
112
|
-
|
|
128
|
+
const emptyDecisions = (configuration.decisions || []).filter((value) => (
|
|
129
|
+
value === "zero-population" || value === "externally-managed"
|
|
130
|
+
));
|
|
131
|
+
throw new Error(emptyDecisions.length
|
|
132
|
+
? `${configuration.title} has no records. Use one of the allowed empty-collection conclusions: ${emptyDecisions.join(", ")}.`
|
|
133
|
+
: `${configuration.title} has no records. Add the required records before confirming this collection.`);
|
|
113
134
|
}
|
|
114
135
|
if (assessment.records.length && decision === "zero-population") {
|
|
115
136
|
throw new Error(`${configuration.title} has ${assessment.records.length} records and cannot be confirmed as a zero population.`);
|
|
@@ -119,11 +140,11 @@ export async function planCollectionReview(input = process.cwd(), options = {})
|
|
|
119
140
|
}
|
|
120
141
|
if (decision === "externally-managed") {
|
|
121
142
|
const system = loaded.resources.find((record) => (
|
|
122
|
-
record.type === "system"
|
|
123
|
-
&& record.id ===
|
|
143
|
+
record.type === (v4 ? "component" : "system")
|
|
144
|
+
&& record.id === authoritativeSourceId
|
|
124
145
|
&& record.status === "active"
|
|
125
146
|
));
|
|
126
|
-
if (!system) throw new Error(`${configuration.title} review needs an active authoritative System.`);
|
|
147
|
+
if (!system) throw new Error(`${configuration.title} review needs an active authoritative ${v4 ? "Component" : "System"}.`);
|
|
127
148
|
}
|
|
128
149
|
const existing = assessment.review;
|
|
129
150
|
const record = {
|
|
@@ -132,7 +153,7 @@ export async function planCollectionReview(input = process.cwd(), options = {})
|
|
|
132
153
|
type: "collection-review",
|
|
133
154
|
title: `${configuration.title} review`,
|
|
134
155
|
resourceType,
|
|
135
|
-
scopeResourceIds: [
|
|
156
|
+
scopeResourceIds: [program.id]
|
|
136
157
|
}),
|
|
137
158
|
status: "active",
|
|
138
159
|
decision,
|
|
@@ -141,9 +162,14 @@ export async function planCollectionReview(input = process.cwd(), options = {})
|
|
|
141
162
|
reviewedOn,
|
|
142
163
|
collectionRevision: assessment.collectionRevision,
|
|
143
164
|
scopeRevision,
|
|
144
|
-
...(decision === "externally-managed"
|
|
165
|
+
...(decision === "externally-managed"
|
|
166
|
+
? { [v4 ? "authoritativeComponentId" : "authoritativeSystemId"]: authoritativeSourceId }
|
|
167
|
+
: {})
|
|
145
168
|
};
|
|
146
|
-
if (decision !== "externally-managed")
|
|
169
|
+
if (decision !== "externally-managed") {
|
|
170
|
+
delete record.authoritativeSystemId;
|
|
171
|
+
delete record.authoritativeComponentId;
|
|
172
|
+
}
|
|
147
173
|
return {
|
|
148
174
|
operation: "collection-review",
|
|
149
175
|
resourceType,
|
|
@@ -170,10 +196,30 @@ export async function applyCollectionReview(input = process.cwd(), options = {})
|
|
|
170
196
|
return {
|
|
171
197
|
...plan,
|
|
172
198
|
result,
|
|
173
|
-
assessment: assessCollectionReview(loaded, plan.resourceType)
|
|
199
|
+
assessment: assessCollectionReview(loaded, plan.resourceType, { programId: options.programId })
|
|
174
200
|
};
|
|
175
201
|
}
|
|
176
202
|
|
|
203
|
+
function scopedCollectionRecords(loaded, resourceType, program) {
|
|
204
|
+
if (String(loaded.model.modelVersion) !== "4") {
|
|
205
|
+
return loaded.resources.filter((record) => record.type === resourceType);
|
|
206
|
+
}
|
|
207
|
+
const components = programComponents(loaded, program);
|
|
208
|
+
const componentIds = new Set(components.map(({ id }) => id));
|
|
209
|
+
const selected = {
|
|
210
|
+
system: new Set(program.systemIds || []),
|
|
211
|
+
component: componentIds,
|
|
212
|
+
framework: new Set(program.frameworkIds || []),
|
|
213
|
+
vendor: new Set(components.map(({ vendorId }) => vendorId).filter(Boolean)),
|
|
214
|
+
asset: new Set(loaded.resources.filter((record) => (
|
|
215
|
+
record.type === "asset" && (record.componentIds || []).some((id) => componentIds.has(id))
|
|
216
|
+
)).map(({ id }) => id))
|
|
217
|
+
}[resourceType];
|
|
218
|
+
return loaded.resources.filter((record) => (
|
|
219
|
+
record.type === resourceType && (!selected || selected.has(record.id))
|
|
220
|
+
));
|
|
221
|
+
}
|
|
222
|
+
|
|
177
223
|
function requiredType(loaded, value) {
|
|
178
224
|
const resourceType = String(value || "").trim();
|
|
179
225
|
if (!loaded.model.collectionReviews?.[resourceType]) {
|
package/src/evidence-packet.js
CHANGED
|
@@ -218,9 +218,12 @@ export async function prepareEvidencePacket(input, options = {}) {
|
|
|
218
218
|
return sourceRevisionValidity.get(revision);
|
|
219
219
|
};
|
|
220
220
|
const evidence = [...evidenceIds].map((id) => evidenceSummary(byId.get(id), byId, revisionIsValid)).filter(Boolean).sort(byTitle);
|
|
221
|
+
const v4 = String(loaded.model.modelVersion) === "4";
|
|
221
222
|
const sourceSystemIds = new Set([
|
|
222
|
-
...(
|
|
223
|
-
|
|
223
|
+
...(v4
|
|
224
|
+
? [...controlIds].flatMap((id) => byId.get(id)?.evidenceSourceComponentIds || [])
|
|
225
|
+
: audit?.systemIds || []),
|
|
226
|
+
...evidence.map((item) => item.sourceComponentId || item.sourceSystemId).filter(Boolean)
|
|
224
227
|
]);
|
|
225
228
|
const sourceSystems = [...sourceSystemIds]
|
|
226
229
|
.map((id) => sourceSystemSummary(byId.get(id), evidence, audit))
|
|
@@ -351,7 +354,7 @@ export async function prepareEvidencePacket(input, options = {}) {
|
|
|
351
354
|
controls: controlIds.size,
|
|
352
355
|
requirements: requirementIds.size,
|
|
353
356
|
systems: audit?.systemIds?.length || 0,
|
|
354
|
-
sourceSystems: sourceSystems.length,
|
|
357
|
+
[v4 ? "sourceComponents" : "sourceSystems"]: sourceSystems.length,
|
|
355
358
|
obligationOccurrences: obligations.length,
|
|
356
359
|
eventRuns: eventRuns.length,
|
|
357
360
|
evidence: evidence.length,
|
|
@@ -367,7 +370,8 @@ export async function prepareEvidencePacket(input, options = {}) {
|
|
|
367
370
|
obligations,
|
|
368
371
|
eventRuns,
|
|
369
372
|
evidence,
|
|
370
|
-
sourceSystems,
|
|
373
|
+
[v4 ? "sourceComponents" : "sourceSystems"]: sourceSystems,
|
|
374
|
+
dataModelVersion: String(loaded.model.modelVersion),
|
|
371
375
|
populations,
|
|
372
376
|
managementPreparation,
|
|
373
377
|
controlCoverage,
|
|
@@ -412,6 +416,11 @@ function recordRelevantToAudit(record, audit, byId, seen = new Set()) {
|
|
|
412
416
|
const auditSystems = new Set(audit.systemIds || []);
|
|
413
417
|
const recordSystems = new Set([...(record.systemIds || []), record.systemId, record.sourceSystemId].filter(Boolean));
|
|
414
418
|
if (recordSystems.size && [...recordSystems].some((id) => auditSystems.has(id))) return true;
|
|
419
|
+
const auditComponents = new Set([...auditSystems].flatMap((systemId) => [...byId.values()]
|
|
420
|
+
.filter((candidate) => candidate.type === "component" && (candidate.systemUses || []).some((use) => use.systemId === systemId))
|
|
421
|
+
.map(({ id }) => id)));
|
|
422
|
+
const recordComponents = new Set([...(record.componentIds || []), record.componentId, record.sourceComponentId].filter(Boolean));
|
|
423
|
+
if (recordComponents.size && [...recordComponents].some((id) => auditComponents.has(id))) return true;
|
|
415
424
|
const auditControls = new Set(audit.controlIds || []);
|
|
416
425
|
const recordControls = controlIdsForRecord(record, byId);
|
|
417
426
|
if (recordControls.size && [...recordControls].some((id) => auditControls.has(id))) return true;
|
|
@@ -454,7 +463,7 @@ function expandEvidenceWorkflowContext(selectedIds, byId) {
|
|
|
454
463
|
for (let index = 0; index < queue.length; index += 1) {
|
|
455
464
|
const record = byId.get(queue[index]);
|
|
456
465
|
enqueue(childrenBySource.get(record?.id));
|
|
457
|
-
if (record?.type === "evidence") enqueue([...(record.sourceResourceIds || []), record.sourceSystemId]);
|
|
466
|
+
if (record?.type === "evidence") enqueue([...(record.sourceResourceIds || []), record.sourceComponentId, record.sourceSystemId]);
|
|
458
467
|
if (record?.type === "audit-population") enqueue([record.sourceEvidenceId, ...(record.controlIds || [])]);
|
|
459
468
|
if (record?.type === "control-test") enqueue([record.populationId, ...(record.sampleEvidenceIds || [])]);
|
|
460
469
|
if (record?.type === "action-item") {
|
|
@@ -505,8 +514,8 @@ export async function writeEvidencePacket(input, packet, options = {}) {
|
|
|
505
514
|
await writePacketFile(output, "index.html", packetHtml(packet), files);
|
|
506
515
|
await writePacketFile(output, "control-matrix.csv", controlMatrixCsv(packet), files);
|
|
507
516
|
await writePacketFile(output, "evidence-index.csv", evidenceIndexCsv(packet), files);
|
|
508
|
-
await writePacketFile(output, "source-system-index.csv", sourceSystemIndexCsv(packet), files);
|
|
509
|
-
await writePacketFile(output, "external-evidence-index.csv", externalEvidenceIndexCsv(packet), files);
|
|
517
|
+
await writePacketFile(output, packet.dataModelVersion === "4" ? "source-component-index.csv" : "source-system-index.csv", sourceSystemIndexCsv(packet), files);
|
|
518
|
+
await writePacketFile(output, packet.dataModelVersion === "4" ? "evidence-artifact-index.csv" : "external-evidence-index.csv", externalEvidenceIndexCsv(packet), files);
|
|
510
519
|
await writePacketFile(output, "population-index.csv", populationIndexCsv(packet), files);
|
|
511
520
|
await writePacketFile(output, "HANDLING.md", packetHandlingMarkdown(packet), files);
|
|
512
521
|
for (const item of packet.records) {
|
|
@@ -587,7 +596,7 @@ async function writeChecksums(output, files) {
|
|
|
587
596
|
|
|
588
597
|
function controlMatrixCsv(packet) {
|
|
589
598
|
return csv([
|
|
590
|
-
["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", "External Evidence IDs", "Control Test IDs", "Test Outcomes", "Population IDs", "Population Counts", "Sample Sizes", "Exception Counts", "Population Evidence IDs", "Sample Evidence IDs"],
|
|
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 === "4" ? "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"],
|
|
591
600
|
...packet.controlCoverage.map((control) => [
|
|
592
601
|
control.id,
|
|
593
602
|
control.code,
|
|
@@ -636,16 +645,17 @@ function packetHandlingMarkdown(packet) {
|
|
|
636
645
|
}
|
|
637
646
|
|
|
638
647
|
function evidenceIndexCsv(packet) {
|
|
648
|
+
const v4 = packet.dataModelVersion === "4";
|
|
639
649
|
return csv([
|
|
640
|
-
["Evidence ID", "Evidence", "Status", "Kind", "Source", "Source System ID", "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"],
|
|
650
|
+
["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"],
|
|
641
651
|
...packet.evidence.map((item) => [
|
|
642
652
|
item.id,
|
|
643
653
|
item.title,
|
|
644
654
|
item.status,
|
|
645
655
|
item.artifactKind,
|
|
646
656
|
item.sourceDescription,
|
|
647
|
-
item.sourceSystemId,
|
|
648
|
-
item.sourceSystem,
|
|
657
|
+
v4 ? item.sourceComponentId : item.sourceSystemId,
|
|
658
|
+
v4 ? item.sourceComponent : item.sourceSystem,
|
|
649
659
|
item.collectedOn,
|
|
650
660
|
item.collectorIds.join("\n"),
|
|
651
661
|
item.verifiedOn,
|
|
@@ -668,9 +678,10 @@ function evidenceIndexCsv(packet) {
|
|
|
668
678
|
}
|
|
669
679
|
|
|
670
680
|
function sourceSystemIndexCsv(packet) {
|
|
681
|
+
const v4 = packet.dataModelVersion === "4";
|
|
671
682
|
return csv([
|
|
672
|
-
["System ID", "System", "Status", "Evidence Source Roles", "Evidence Access Owner IDs", "Vendor ID", "In Audit Scope", "Evidence IDs"],
|
|
673
|
-
...packet.sourceSystems.map((item) => [
|
|
683
|
+
[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
|
+
...(packet.sourceComponents || packet.sourceSystems || []).map((item) => [
|
|
674
685
|
item.id,
|
|
675
686
|
item.title,
|
|
676
687
|
item.status,
|
|
@@ -684,15 +695,16 @@ function sourceSystemIndexCsv(packet) {
|
|
|
684
695
|
}
|
|
685
696
|
|
|
686
697
|
function externalEvidenceIndexCsv(packet) {
|
|
698
|
+
const v4 = packet.dataModelVersion === "4";
|
|
687
699
|
return csv([
|
|
688
|
-
["Evidence ID", "Evidence", "Source System ID", "Source System", "Control IDs", "External Reference", "Fixed Attachment Included", "Delivery Note"],
|
|
700
|
+
["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"],
|
|
689
701
|
...packet.evidence
|
|
690
702
|
.filter((item) => item.externalReference)
|
|
691
703
|
.map((item) => [
|
|
692
704
|
item.id,
|
|
693
705
|
item.title,
|
|
694
|
-
item.sourceSystemId,
|
|
695
|
-
item.sourceSystem,
|
|
706
|
+
v4 ? item.sourceComponentId : item.sourceSystemId,
|
|
707
|
+
v4 ? item.sourceComponent : item.sourceSystem,
|
|
696
708
|
item.controlIds.join("\n"),
|
|
697
709
|
JSON.stringify(item.externalReference),
|
|
698
710
|
item.filePaths.length ? "yes" : "no",
|
|
@@ -704,8 +716,9 @@ function externalEvidenceIndexCsv(packet) {
|
|
|
704
716
|
}
|
|
705
717
|
|
|
706
718
|
function populationIndexCsv(packet) {
|
|
719
|
+
const v4 = packet.dataModelVersion === "4";
|
|
707
720
|
return csv([
|
|
708
|
-
["Population ID", "Population", "Kind", "Status", "Period Start", "Period End", "Source System ID", "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"],
|
|
721
|
+
["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"],
|
|
709
722
|
...packet.populations.map((item) => [
|
|
710
723
|
item.id,
|
|
711
724
|
item.title,
|
|
@@ -713,8 +726,8 @@ function populationIndexCsv(packet) {
|
|
|
713
726
|
item.status,
|
|
714
727
|
item.periodStart,
|
|
715
728
|
item.periodEnd,
|
|
716
|
-
item.sourceSystemId,
|
|
717
|
-
item.sourceSystem,
|
|
729
|
+
v4 ? item.sourceComponentId : item.sourceSystemId,
|
|
730
|
+
v4 ? item.sourceComponent : item.sourceSystem,
|
|
718
731
|
item.sourceDescription,
|
|
719
732
|
item.queryDescription,
|
|
720
733
|
item.timezone,
|
|
@@ -1018,7 +1031,7 @@ function packetGaps({
|
|
|
1018
1031
|
}
|
|
1019
1032
|
}
|
|
1020
1033
|
if (controlNeedsExternalEvidence(control, model) && !coverage.evidenceIds.length) {
|
|
1021
|
-
gaps.push(gap("error", "control-missing-external-evidence", `${coverage.code || coverage.title} relies on an external
|
|
1034
|
+
gaps.push(gap("error", "control-missing-external-evidence", `${coverage.code || coverage.title} relies on an external source but has no linked ${String(model.modelVersion) === "4" ? "Evidence Artifact" : "External Evidence"} in the packet.`, coverage.id));
|
|
1022
1035
|
} else if (!controlNeedsExternalEvidence(control, model) && !coverage.operatingRecordIds.length) {
|
|
1023
1036
|
gaps.push(gap("error", "control-missing-filegrc-evidence", `${coverage.code || coverage.title} has no dated filegrc operating record in the packet.`, coverage.id));
|
|
1024
1037
|
}
|
|
@@ -1219,10 +1232,12 @@ function packetGaps({
|
|
|
1219
1232
|
const dateLabel = audit?.auditKind === "soc-2-type-1" ? `the ${start} as-of date` : `${start} through ${end}`;
|
|
1220
1233
|
gaps.push(gap("error", "evidence-outside-engagement-date", `${item.title} is linked to a control but does not cover ${dateLabel}.`, item.id));
|
|
1221
1234
|
}
|
|
1222
|
-
if (item.sourceSystemId) {
|
|
1223
|
-
const
|
|
1224
|
-
|
|
1225
|
-
|
|
1235
|
+
if (item.sourceComponentId || item.sourceSystemId) {
|
|
1236
|
+
const sourceId = item.sourceComponentId || item.sourceSystemId;
|
|
1237
|
+
const sourceSystem = byId.get(sourceId);
|
|
1238
|
+
const expectedType = item.sourceComponentId ? "component" : "system";
|
|
1239
|
+
if (!sourceSystem || sourceSystem.type !== expectedType) {
|
|
1240
|
+
gaps.push(gap("error", "evidence-source-missing", `${item.title} does not resolve to a cataloged source ${expectedType}.`, item.id));
|
|
1226
1241
|
} else {
|
|
1227
1242
|
if (!["active", "deprecated"].includes(sourceSystem.status)) {
|
|
1228
1243
|
gaps.push(gap("warning", "evidence-source-system-inactive", `${item.title} came from ${sourceSystem.title}, which is ${sourceSystem.status}. Confirm that this was the authoritative source when the evidence was generated.`, item.id));
|
|
@@ -1232,7 +1247,7 @@ function packetGaps({
|
|
|
1232
1247
|
}
|
|
1233
1248
|
}
|
|
1234
1249
|
} else if (["population-export", "system-export", "configuration-export"].includes(item.artifactKind)) {
|
|
1235
|
-
gaps.push(gap("error", "evidence-source-system-unrecorded", `${item.title} is a source
|
|
1250
|
+
gaps.push(gap("error", "evidence-source-system-unrecorded", `${item.title} is a source export but does not link the cataloged ${String(model.modelVersion) === "4" ? "source Component" : "system of record"}.`, item.id));
|
|
1236
1251
|
}
|
|
1237
1252
|
if (item.externalReference && !item.filePaths.length) {
|
|
1238
1253
|
gaps.push(gap("warning", "external-only-evidence", `${item.title} relies on an external reference and is not self-contained in the packet.`, item.id));
|
|
@@ -1433,6 +1448,8 @@ function evidenceSummary(record, byId, revisionIsValid) {
|
|
|
1433
1448
|
capture: record.capture || null,
|
|
1434
1449
|
sourceSystemId: record.sourceSystemId || null,
|
|
1435
1450
|
sourceSystem: byId.get(record.sourceSystemId)?.title || null,
|
|
1451
|
+
sourceComponentId: record.sourceComponentId || null,
|
|
1452
|
+
sourceComponent: byId.get(record.sourceComponentId)?.title || null,
|
|
1436
1453
|
collectorIds: record.collectorIds || [],
|
|
1437
1454
|
verifierIds: record.verifierIds || [],
|
|
1438
1455
|
verifiedOn: record.verifiedOn || null,
|
|
@@ -1449,6 +1466,7 @@ function evidenceSummary(record, byId, revisionIsValid) {
|
|
|
1449
1466
|
function populationSummary(record, byId) {
|
|
1450
1467
|
const evidence = byId.get(record.sourceEvidenceId);
|
|
1451
1468
|
const sourceSystemId = record.sourceSystemId || evidence?.sourceSystemId || null;
|
|
1469
|
+
const sourceComponentId = record.sourceComponentId || evidence?.sourceComponentId || null;
|
|
1452
1470
|
return {
|
|
1453
1471
|
id: record.id,
|
|
1454
1472
|
title: record.title,
|
|
@@ -1461,6 +1479,8 @@ function populationSummary(record, byId) {
|
|
|
1461
1479
|
controlIds: record.controlIds || [],
|
|
1462
1480
|
sourceSystemId,
|
|
1463
1481
|
sourceSystem: byId.get(sourceSystemId)?.title || null,
|
|
1482
|
+
sourceComponentId,
|
|
1483
|
+
sourceComponent: byId.get(sourceComponentId)?.title || null,
|
|
1464
1484
|
sourceEvidenceId: record.sourceEvidenceId || null,
|
|
1465
1485
|
source: evidence?.sourceDescription || null,
|
|
1466
1486
|
populationCount: evidence?.populationCount ?? null,
|
|
@@ -1477,7 +1497,7 @@ function populationSummary(record, byId) {
|
|
|
1477
1497
|
}
|
|
1478
1498
|
|
|
1479
1499
|
function sourceSystemSummary(record, evidence, audit) {
|
|
1480
|
-
if (!record || record.type
|
|
1500
|
+
if (!record || !["system", "component"].includes(record.type)) return null;
|
|
1481
1501
|
return {
|
|
1482
1502
|
id: record.id,
|
|
1483
1503
|
title: record.title,
|
|
@@ -1485,8 +1505,11 @@ function sourceSystemSummary(record, evidence, audit) {
|
|
|
1485
1505
|
evidenceSourceKinds: record.evidenceSourceKinds || [],
|
|
1486
1506
|
evidenceOwnerIds: record.evidenceOwnerIds || [],
|
|
1487
1507
|
vendorId: record.vendorId || null,
|
|
1488
|
-
inAuditScope:
|
|
1489
|
-
|
|
1508
|
+
inAuditScope: record.type === "system"
|
|
1509
|
+
? (audit?.systemIds || []).includes(record.id)
|
|
1510
|
+
: (record.systemUses || []).some(({ systemId }) => (audit?.systemIds || []).includes(systemId)),
|
|
1511
|
+
evidenceIds: evidence.filter((item) => (item.sourceComponentId || item.sourceSystemId) === record.id).map((item) => item.id),
|
|
1512
|
+
resourceType: record.type
|
|
1490
1513
|
};
|
|
1491
1514
|
}
|
|
1492
1515
|
|
|
@@ -1545,13 +1568,15 @@ function populationGaps(gaps, audit, populations, byId, model) {
|
|
|
1545
1568
|
if (population.reconciledOn && generatedOn && population.reconciledOn < generatedOn) {
|
|
1546
1569
|
gaps.push(gap("error", "population-reconciled-before-generation", `${population.title} was reconciled before its population export was generated.`, population.id));
|
|
1547
1570
|
}
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
gaps.push(gap("error", "population-source-
|
|
1571
|
+
const populationSourceId = population.sourceComponentId || population.sourceSystemId;
|
|
1572
|
+
const evidenceSourceId = evidence.sourceComponentId || evidence.sourceSystemId;
|
|
1573
|
+
if (!populationSourceId) {
|
|
1574
|
+
gaps.push(gap("error", "population-source-missing", `${population.title} does not identify its authoritative source Component.`, population.id));
|
|
1575
|
+
} else if (evidenceSourceId !== populationSourceId) {
|
|
1576
|
+
gaps.push(gap("error", "population-source-mismatch", `${population.title} and ${evidence.title} do not name the same authoritative source Component.`, population.id));
|
|
1552
1577
|
}
|
|
1553
1578
|
const template = expected.find((item) => item.kind === population.populationKind);
|
|
1554
|
-
const sourceSystem = byId.get(
|
|
1579
|
+
const sourceSystem = byId.get(populationSourceId);
|
|
1555
1580
|
if (template?.sourceKind && sourceSystem && !(sourceSystem.evidenceSourceKinds || []).includes(template.sourceKind)) {
|
|
1556
1581
|
gaps.push(gap("error", "population-source-role-mismatch", `${sourceSystem.title} is not cataloged for the ${displaySourceKind(template.sourceKind)} evidence role required by ${population.title}.`, sourceSystem.id));
|
|
1557
1582
|
}
|
|
@@ -1594,6 +1619,7 @@ function recordSummary(record) {
|
|
|
1594
1619
|
}
|
|
1595
1620
|
|
|
1596
1621
|
function packetMarkdown(packet) {
|
|
1622
|
+
const v4 = packet.dataModelVersion === "4";
|
|
1597
1623
|
const readiness = packet.readiness.status === "delivery-ready"
|
|
1598
1624
|
? "filegrc management checks passed. The engagement team still determines whether the evidence is sufficient and appropriate."
|
|
1599
1625
|
: `${packet.readiness.errors} errors and ${packet.readiness.warnings} warnings require review. This is a draft packet.`;
|
|
@@ -1618,15 +1644,17 @@ function packetMarkdown(packet) {
|
|
|
1618
1644
|
`- ${packet.summary.filegrcRecords} filegrc Evidence records`,
|
|
1619
1645
|
`- ${packet.summary.obligationOccurrences} recurring obligation occurrences`,
|
|
1620
1646
|
`- ${packet.summary.eventRuns} event runs`,
|
|
1621
|
-
`- ${packet.summary.evidence} External Evidence records`,
|
|
1647
|
+
`- ${packet.summary.evidence} ${v4 ? "Evidence Artifact" : "External Evidence"} records`,
|
|
1622
1648
|
`- ${packet.summary.populations} reconciled or planned populations`,
|
|
1623
1649
|
`- ${packet.summary.policies} policies`,
|
|
1624
1650
|
`- ${packet.summary.controls} controls`,
|
|
1625
1651
|
`- ${packet.summary.requirements} criteria`,
|
|
1626
1652
|
`- ${packet.summary.systems} in-scope systems`,
|
|
1627
|
-
`- ${packet.summary.sourceSystems} cataloged source
|
|
1653
|
+
`- ${v4 ? packet.summary.sourceComponents : packet.summary.sourceSystems} cataloged source ${v4 ? "Components" : "Systems"}`,
|
|
1628
1654
|
"",
|
|
1629
|
-
|
|
1655
|
+
v4
|
|
1656
|
+
? "Open `index.html` for the auditor-oriented index. `control-matrix.csv` cross-references criteria, Controls, filegrc Evidence, Evidence Artifacts, and tests. `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
|
+
: "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.",
|
|
1630
1658
|
"",
|
|
1631
1659
|
"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.",
|
|
1632
1660
|
""
|
|
@@ -1635,6 +1663,8 @@ function packetMarkdown(packet) {
|
|
|
1635
1663
|
}
|
|
1636
1664
|
|
|
1637
1665
|
function packetHtml(packet) {
|
|
1666
|
+
const v4 = packet.dataModelVersion === "4";
|
|
1667
|
+
const artifactLabel = v4 ? "Evidence Artifact" : "External Evidence";
|
|
1638
1668
|
const section = (title, body) => `<section><h2>${escapeHtml(title)}</h2>${body}</section>`;
|
|
1639
1669
|
const links = (items) => items.length
|
|
1640
1670
|
? `<ul>${items.map((item) => `<li><a href="records/${encodeURIComponent(item.type)}/${encodeURIComponent(item.id)}.json">${escapeHtml(item.title)}</a><small>${escapeHtml(item.type)}</small></li>`).join("")}</ul>`
|
|
@@ -1652,11 +1682,14 @@ function packetHtml(packet) {
|
|
|
1652
1682
|
? `<table><thead><tr><th>Obligation</th><th>Allowed window</th><th>Status</th></tr></thead><tbody>${packet.obligations.map((item) => `<tr><td>${escapeHtml(item.title)}</td><td>${item.dueWindowStart} through ${item.dueWindowEnd}<br><small>Overdue ${item.overdueOn}</small></td><td>${escapeHtml(item.status)}</td></tr>`).join("")}</tbody></table>`
|
|
1653
1683
|
: "<p>No recurring occurrences intersect this period.</p>";
|
|
1654
1684
|
const evidence = packet.evidence.length
|
|
1655
|
-
? `<table><thead><tr><th
|
|
1656
|
-
:
|
|
1657
|
-
const
|
|
1658
|
-
|
|
1659
|
-
|
|
1685
|
+
? `<table><thead><tr><th>${artifactLabel}</th><th>Source and period</th><th>Controls</th><th>Files</th></tr></thead><tbody>${packet.evidence.map((item) => `<tr><td><a href="records/evidence/${encodeURIComponent(item.id)}.json">${escapeHtml(item.title)}</a><small>${escapeHtml(item.status)} · ${escapeHtml(item.artifactKind)}</small></td><td>${escapeHtml(item.sourceDescription)}<small>${escapeHtml(item.periodStart || item.collectedOn)}${item.periodEnd ? ` through ${escapeHtml(item.periodEnd)}` : ""}</small></td><td>${item.controlIds.map(escapeHtml).join("<br>") || "None"}</td><td>${item.filePaths.map((path) => `<a class="attachment" href="attachments/${path.split("/").map(encodeURIComponent).join("/")}">${escapeHtml(basename(path))}</a>`).join("") || "No fixed attachment"}</td></tr>`).join("")}</tbody></table>`
|
|
1686
|
+
: `<p>No ${artifactLabel} records were selected.</p>`;
|
|
1687
|
+
const packetSources = packet.sourceComponents || packet.sourceSystems || [];
|
|
1688
|
+
const sourceIndex = v4 ? "source-component-index.csv" : "source-system-index.csv";
|
|
1689
|
+
const artifactIndex = v4 ? "evidence-artifact-index.csv" : "external-evidence-index.csv";
|
|
1690
|
+
const sourceSystems = packetSources.length
|
|
1691
|
+
? `<p><a href="${sourceIndex}">Download source ${v4 ? "Component" : "System"} index CSV</a></p><table><thead><tr><th>${v4 ? "Source Component" : "System of record"}</th><th>Evidence roles</th><th>Audit relationship</th><th>Evidence</th></tr></thead><tbody>${packetSources.map((item) => `<tr><td><a href="records/${encodeURIComponent(item.resourceType)}/${encodeURIComponent(item.id)}.json">${escapeHtml(item.title)}</a><small>${escapeHtml(item.status)}</small></td><td>${item.evidenceSourceKinds.map(escapeHtml).join("<br>") || "No evidence role recorded"}</td><td>${item.inAuditScope ? (v4 ? "Supports an in-scope System" : "In-scope system") : "Evidence source"}</td><td>${item.evidenceIds.length}</td></tr>`).join("")}</tbody></table><p><a href="${artifactIndex}">Download ${artifactLabel} delivery index CSV</a></p>`
|
|
1692
|
+
: `<p>No source ${v4 ? "Components" : "Systems"} were cataloged.</p>`;
|
|
1660
1693
|
const populations = packet.populations.length
|
|
1661
1694
|
? `<p><a href="population-index.csv">Download population index CSV</a></p><table><thead><tr><th>Population</th><th>Period and source</th><th>Count</th><th>Reconciliation</th></tr></thead><tbody>${packet.populations.map((item) => `<tr><td><a href="records/audit-population/${encodeURIComponent(item.id)}.json">${escapeHtml(item.title)}</a><small>${escapeHtml(item.status)} · ${escapeHtml(item.populationKind)}</small></td><td>${escapeHtml(item.periodStart)} through ${escapeHtml(item.periodEnd)}<small>${escapeHtml(item.source || "No authoritative source recorded")}</small></td><td>${item.populationCount ?? "Not recorded"}</td><td>${escapeHtml(item.conclusion || item.notApplicableReason || "Not complete")}</td></tr>`).join("")}</tbody></table>`
|
|
1662
1695
|
: "<p>No audit populations were selected.</p>";
|
|
@@ -1674,7 +1707,7 @@ function packetHtml(packet) {
|
|
|
1674
1707
|
}).join("")}</tbody></table>`
|
|
1675
1708
|
: "<p>No filegrc Evidence records matched this period.</p>";
|
|
1676
1709
|
const controlCoverage = packet.controlCoverage.length
|
|
1677
|
-
? `<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
|
|
1710
|
+
? `<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>`
|
|
1678
1711
|
: "<p>No controls were selected.</p>";
|
|
1679
1712
|
const readinessLabel = packet.readiness.status === "delivery-ready" ? "filegrc management checks passed" : "Draft, do not deliver";
|
|
1680
1713
|
const packetDate = packet.period.basis === "as-of"
|
|
@@ -1682,7 +1715,7 @@ function packetHtml(packet) {
|
|
|
1682
1715
|
: `${escapeHtml(packet.period.start)} through ${escapeHtml(packet.period.end)}`;
|
|
1683
1716
|
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Evidence packet</title><style>
|
|
1684
1717
|
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}
|
|
1685
|
-
</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("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("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>`;
|
|
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>`;
|
|
1686
1719
|
}
|
|
1687
1720
|
|
|
1688
1721
|
async function writePacketFile(output, relativePath, source, files) {
|
package/src/external-reviewer.js
CHANGED
|
@@ -5,8 +5,8 @@ import { loadWorkspace } from "./workspace.js";
|
|
|
5
5
|
|
|
6
6
|
export async function scaffoldExternalReviewerGovernance(input = process.cwd()) {
|
|
7
7
|
const loaded = await loadWorkspace(input);
|
|
8
|
-
if (String(loaded.model.modelVersion)
|
|
9
|
-
throw new Error("External reviewer setup requires a model v3 workspace.");
|
|
8
|
+
if (!["3", "4"].includes(String(loaded.model.modelVersion))) {
|
|
9
|
+
throw new Error("External reviewer setup requires a model v3 or v4 workspace.");
|
|
10
10
|
}
|
|
11
11
|
return {
|
|
12
12
|
reviewerName: null,
|
|
@@ -22,8 +22,8 @@ export async function scaffoldExternalReviewerGovernance(input = process.cwd())
|
|
|
22
22
|
|
|
23
23
|
export async function planExternalReviewerGovernance(input = process.cwd(), options = {}) {
|
|
24
24
|
const loaded = await loadWorkspace(input);
|
|
25
|
-
if (String(loaded.model.modelVersion)
|
|
26
|
-
throw new Error("External reviewer setup requires a model v3 workspace.");
|
|
25
|
+
if (!["3", "4"].includes(String(loaded.model.modelVersion))) {
|
|
26
|
+
throw new Error("External reviewer setup requires a model v3 or v4 workspace.");
|
|
27
27
|
}
|
|
28
28
|
const name = required(options.reviewerName, "External reviewer name");
|
|
29
29
|
const startsOn = required(options.startsOn, "Appointment start date");
|