filegrc 0.3.4 → 0.5.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 +24 -6
- package/model/index.js +41 -3
- package/model/v1.json +81 -47
- package/model/v2.json +8022 -0
- package/model/v3.json +9391 -0
- package/package.json +2 -2
- package/src/agent.js +89 -8
- package/src/appointments.js +19 -0
- package/src/audit-preparation.js +109 -65
- package/src/audit-transition.js +96 -0
- package/src/batch-review.js +109 -0
- package/src/cli.js +563 -148
- package/src/collection-review.js +185 -0
- package/src/coverage.js +50 -0
- package/src/evidence-packet.js +149 -77
- package/src/external-reviewer.js +165 -0
- package/src/files.js +267 -29
- package/src/git.js +239 -41
- package/src/index.js +41 -7
- package/src/model-docs.js +103 -7
- package/src/model-migration.js +1958 -0
- package/src/mutation.js +42 -0
- package/src/obligations.js +502 -95
- package/src/parties.js +17 -2
- package/src/program-lifecycle.js +1 -0
- package/src/program-path.js +70 -60
- package/src/program-readiness.js +470 -130
- package/src/reconciliation.js +277 -0
- package/src/resource-status.js +17 -0
- package/src/server.js +347 -48
- package/src/setup.js +57 -26
- package/src/source-coverage.js +61 -0
- package/src/state.js +122 -25
- package/src/timing.js +41 -0
- package/src/validate.js +707 -44
- package/src/web.js +1440 -304
- package/src/workflow.js +1595 -0
- package/src/workspace.js +15 -7
- package/src/evidence-tests.js +0 -69
package/src/evidence-packet.js
CHANGED
|
@@ -3,6 +3,13 @@ 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
5
|
import { assessAuditPreparation } from "./audit-preparation.js";
|
|
6
|
+
import {
|
|
7
|
+
coverageContains,
|
|
8
|
+
coverageEnd,
|
|
9
|
+
coverageMatches,
|
|
10
|
+
coverageOverlaps,
|
|
11
|
+
coverageStart
|
|
12
|
+
} from "./coverage.js";
|
|
6
13
|
import { getFileAtRevision, getGitSummary, getWorkspaceHistories, hasGitRevision } from "./git.js";
|
|
7
14
|
import { planObligations } from "./obligations.js";
|
|
8
15
|
import { isWithin, resolveDataPath, resolveWorkspacePath } from "./paths.js";
|
|
@@ -12,6 +19,7 @@ import { serializeWorkspaceMutation } from "./mutation.js";
|
|
|
12
19
|
import { validateWorkspace } from "./validate.js";
|
|
13
20
|
|
|
14
21
|
const NON_EVIDENCE_RECORD_TYPES = new Set([
|
|
22
|
+
"appointment",
|
|
15
23
|
"audit",
|
|
16
24
|
"audit-population",
|
|
17
25
|
"audit-request",
|
|
@@ -60,7 +68,8 @@ export async function prepareEvidencePacket(input, options = {}) {
|
|
|
60
68
|
asOf: end,
|
|
61
69
|
from: start,
|
|
62
70
|
through: end,
|
|
63
|
-
includeComplete: true
|
|
71
|
+
includeComplete: true,
|
|
72
|
+
model: loaded.model
|
|
64
73
|
});
|
|
65
74
|
const obligations = (typeOne ? [] : plan.calendarItems).filter((item) => (
|
|
66
75
|
item.dueWindowStart <= end
|
|
@@ -88,8 +97,6 @@ export async function prepareEvidencePacket(input, options = {}) {
|
|
|
88
97
|
...(audit.systemIds || []),
|
|
89
98
|
...(audit.requirementIds || []),
|
|
90
99
|
...(audit.controlIds || []),
|
|
91
|
-
...(audit.controlTestIds || []),
|
|
92
|
-
...(audit.evidenceIds || []),
|
|
93
100
|
...(audit.contactIds || []),
|
|
94
101
|
...(audit.complementaryControlIds || []),
|
|
95
102
|
...(audit.subserviceVendorIds || []),
|
|
@@ -104,6 +111,11 @@ export async function prepareEvidencePacket(input, options = {}) {
|
|
|
104
111
|
for (const request of records.filter((record) => record.type === "audit-request" && record.auditId === audit.id)) {
|
|
105
112
|
selectedIds.add(request.id);
|
|
106
113
|
}
|
|
114
|
+
for (const record of records.filter((candidate) => (
|
|
115
|
+
candidate.auditId === audit.id || (candidate.auditIds || []).includes(audit.id)
|
|
116
|
+
))) {
|
|
117
|
+
selectedIds.add(record.id);
|
|
118
|
+
}
|
|
107
119
|
for (const record of records.filter((candidate) => ["finding", "action-item"].includes(candidate.type))) {
|
|
108
120
|
if (recordRelevantToAudit(record, audit, byId)) selectedIds.add(record.id);
|
|
109
121
|
}
|
|
@@ -157,8 +169,20 @@ export async function prepareEvidencePacket(input, options = {}) {
|
|
|
157
169
|
for (const controlId of controlIds) {
|
|
158
170
|
const control = byId.get(controlId);
|
|
159
171
|
addIds(selectedIds, control?.systemIds);
|
|
160
|
-
|
|
161
|
-
|
|
172
|
+
}
|
|
173
|
+
for (const record of records) {
|
|
174
|
+
if (
|
|
175
|
+
record.type === "commitment"
|
|
176
|
+
&& (record.controlIds || []).some((id) => controlIds.has(id))
|
|
177
|
+
) {
|
|
178
|
+
selectedIds.add(record.id);
|
|
179
|
+
}
|
|
180
|
+
if (
|
|
181
|
+
record.type === "risk"
|
|
182
|
+
&& (record.controlIds || []).some((id) => controlIds.has(id))
|
|
183
|
+
) {
|
|
184
|
+
selectedIds.add(record.id);
|
|
185
|
+
}
|
|
162
186
|
}
|
|
163
187
|
for (const complementaryControl of records.filter((record) => record.type === "complementary-control")) {
|
|
164
188
|
if ((complementaryControl.relatedControlIds || []).some((id) => controlIds.has(id))) {
|
|
@@ -167,8 +191,12 @@ export async function prepareEvidencePacket(input, options = {}) {
|
|
|
167
191
|
}
|
|
168
192
|
for (const systemId of audit?.systemIds || []) {
|
|
169
193
|
const system = byId.get(systemId);
|
|
170
|
-
addIds(selectedIds, system?.commitmentIds);
|
|
171
194
|
addIds(selectedIds, system?.subserviceVendorIds);
|
|
195
|
+
for (const commitment of records.filter((record) => (
|
|
196
|
+
record.type === "commitment" && (record.systemIds || []).includes(systemId)
|
|
197
|
+
))) {
|
|
198
|
+
selectedIds.add(commitment.id);
|
|
199
|
+
}
|
|
172
200
|
}
|
|
173
201
|
|
|
174
202
|
const policyIds = new Set(audit
|
|
@@ -292,9 +320,7 @@ export async function prepareEvidencePacket(input, options = {}) {
|
|
|
292
320
|
kind: audit.auditKind,
|
|
293
321
|
status: audit.status,
|
|
294
322
|
scope: audit.scope,
|
|
295
|
-
|
|
296
|
-
periodEnd: audit.periodEnd,
|
|
297
|
-
typeOneAsOf: audit.typeOneAsOf,
|
|
323
|
+
coverage: audit.coverage,
|
|
298
324
|
systemIds: audit.systemIds || [],
|
|
299
325
|
requirementIds: audit.requirementIds || [],
|
|
300
326
|
controlIds: audit.controlIds || [],
|
|
@@ -313,7 +339,7 @@ export async function prepareEvidencePacket(input, options = {}) {
|
|
|
313
339
|
dataDigest
|
|
314
340
|
},
|
|
315
341
|
handling: {
|
|
316
|
-
classifications: [...new Set(evidence.map(({
|
|
342
|
+
classifications: [...new Set(evidence.map(({ classificationId }) => classificationId).filter(Boolean))].sort(),
|
|
317
343
|
containsExternalReferences: evidence.some(({ externalReference }) => Boolean(externalReference)),
|
|
318
344
|
encrypted: false
|
|
319
345
|
},
|
|
@@ -371,8 +397,6 @@ function recordRelevantToAudit(record, audit, byId, seen = new Set()) {
|
|
|
371
397
|
...(audit.systemIds || []),
|
|
372
398
|
...(audit.requirementIds || []),
|
|
373
399
|
...(audit.controlIds || []),
|
|
374
|
-
...(audit.controlTestIds || []),
|
|
375
|
-
...(audit.evidenceIds || []),
|
|
376
400
|
...(audit.contactIds || []),
|
|
377
401
|
...(audit.complementaryControlIds || []),
|
|
378
402
|
...(audit.subserviceVendorIds || []),
|
|
@@ -395,8 +419,14 @@ function recordRelevantToAudit(record, audit, byId, seen = new Set()) {
|
|
|
395
419
|
if ((record.requirementIds || []).some((id) => auditRequirements.has(id))) return true;
|
|
396
420
|
const auditControlRecords = [...auditControls].map((id) => byId.get(id)).filter(Boolean);
|
|
397
421
|
const policyIds = new Set(auditControlRecords.flatMap((control) => control.policyIds || []));
|
|
398
|
-
const commitmentIds = new Set(
|
|
399
|
-
|
|
422
|
+
const commitmentIds = new Set([...byId.values()]
|
|
423
|
+
.filter((candidate) => candidate.type === "commitment"
|
|
424
|
+
&& (candidate.controlIds || []).some((id) => auditControls.has(id)))
|
|
425
|
+
.map(({ id }) => id));
|
|
426
|
+
const riskIds = new Set([...byId.values()]
|
|
427
|
+
.filter((candidate) => candidate.type === "risk"
|
|
428
|
+
&& (candidate.controlIds || []).some((id) => auditControls.has(id)))
|
|
429
|
+
.map(({ id }) => id));
|
|
400
430
|
if ((record.type === "policy" && policyIds.has(record.id)) || (record.policyIds || []).some((id) => policyIds.has(id))) return true;
|
|
401
431
|
if ((record.type === "commitment" && commitmentIds.has(record.id)) || (record.commitmentIds || []).some((id) => commitmentIds.has(id))) return true;
|
|
402
432
|
if ((record.type === "risk" && riskIds.has(record.id)) || (record.riskIds || []).some((id) => riskIds.has(id))) return true;
|
|
@@ -557,7 +587,7 @@ async function writeChecksums(output, files) {
|
|
|
557
587
|
|
|
558
588
|
function controlMatrixCsv(packet) {
|
|
559
589
|
return csv([
|
|
560
|
-
["Control ID", "Code", "Control", "Control Statement", "Operating Activity", "Status", "Effective On", "
|
|
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"],
|
|
561
591
|
...packet.controlCoverage.map((control) => [
|
|
562
592
|
control.id,
|
|
563
593
|
control.code,
|
|
@@ -566,7 +596,7 @@ function controlMatrixCsv(packet) {
|
|
|
566
596
|
control.activity,
|
|
567
597
|
control.status,
|
|
568
598
|
control.effectiveOn,
|
|
569
|
-
control.
|
|
599
|
+
control.operationPattern,
|
|
570
600
|
control.operationMode,
|
|
571
601
|
control.systemIds.join("\n"),
|
|
572
602
|
control.requirementIds.join("\n"),
|
|
@@ -612,8 +642,8 @@ function evidenceIndexCsv(packet) {
|
|
|
612
642
|
item.id,
|
|
613
643
|
item.title,
|
|
614
644
|
item.status,
|
|
615
|
-
item.
|
|
616
|
-
item.
|
|
645
|
+
item.artifactKind,
|
|
646
|
+
item.sourceDescription,
|
|
617
647
|
item.sourceSystemId,
|
|
618
648
|
item.sourceSystem,
|
|
619
649
|
item.collectedOn,
|
|
@@ -685,7 +715,7 @@ function populationIndexCsv(packet) {
|
|
|
685
715
|
item.periodEnd,
|
|
686
716
|
item.sourceSystemId,
|
|
687
717
|
item.sourceSystem,
|
|
688
|
-
item.
|
|
718
|
+
item.sourceDescription,
|
|
689
719
|
item.queryDescription,
|
|
690
720
|
item.timezone,
|
|
691
721
|
item.generatedAt,
|
|
@@ -799,19 +829,18 @@ function packetRecord(record, model, start, end, timezone) {
|
|
|
799
829
|
if (date && date >= start && date <= end) dates.push({ field: name, value, date });
|
|
800
830
|
}
|
|
801
831
|
}
|
|
802
|
-
const overlaps =
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
&& record.periodEnd >= start;
|
|
832
|
+
const overlaps = coverageOverlaps(record.coverage, start, end);
|
|
833
|
+
const coverageStartDate = coverageStart(record.coverage);
|
|
834
|
+
const coverageEndDate = coverageEnd(record.coverage);
|
|
806
835
|
if (!dates.length && !overlaps) return null;
|
|
807
836
|
dates.sort((a, b) => (a.date || a.value).localeCompare(b.date || b.value) || a.field.localeCompare(b.field));
|
|
808
837
|
return {
|
|
809
838
|
id: record.id,
|
|
810
839
|
type: record.type,
|
|
811
840
|
title: record.title,
|
|
812
|
-
primaryDate: dates[0]?.date || dates[0]?.value ||
|
|
841
|
+
primaryDate: dates[0]?.date || dates[0]?.value || coverageStartDate,
|
|
813
842
|
dates,
|
|
814
|
-
...(overlaps ? { period: { start:
|
|
843
|
+
...(overlaps ? { period: { start: coverageStartDate, end: coverageEndDate } } : {})
|
|
815
844
|
};
|
|
816
845
|
}
|
|
817
846
|
|
|
@@ -848,14 +877,21 @@ function buildControlCoverage({ audit, byId, controlIds, evidenceIds, model, rec
|
|
|
848
877
|
.filter((record) => record.type === "control-test" && record.controlId === controlId)
|
|
849
878
|
.filter((record) => (
|
|
850
879
|
record.auditId === audit?.id
|
|
851
|
-
|| (!record.auditId && record.
|
|
852
|
-
|| (!record.auditId && record.asOfDate >= start && record.asOfDate <= end)
|
|
880
|
+
|| (!record.auditId && coverageOverlaps(record.coverage, start, end))
|
|
853
881
|
));
|
|
854
882
|
const operatingRecords = records.filter((record) => (
|
|
855
883
|
!NON_EVIDENCE_RECORD_TYPES.has(record.type)
|
|
856
884
|
&& controlIdsForRecord(record, byId).has(controlId)
|
|
857
885
|
&& packetRecord(record, model, start, end, timezone)
|
|
858
886
|
));
|
|
887
|
+
const zeroPopulationRecords = records.filter((record) => (
|
|
888
|
+
record.type === "audit-population"
|
|
889
|
+
&& record.auditId === audit?.id
|
|
890
|
+
&& record.status === "reconciled"
|
|
891
|
+
&& record.conclusion === "complete"
|
|
892
|
+
&& (record.controlIds || []).includes(controlId)
|
|
893
|
+
&& byId.get(record.sourceEvidenceId)?.populationCount === 0
|
|
894
|
+
));
|
|
859
895
|
const linkedEvidenceIds = new Set(evidenceByControl.get(controlId) || []);
|
|
860
896
|
for (const test of tests) {
|
|
861
897
|
addIds(linkedEvidenceIds, test.evidenceIds);
|
|
@@ -871,22 +907,23 @@ function buildControlCoverage({ audit, byId, controlIds, evidenceIds, model, rec
|
|
|
871
907
|
activity: control?.activity || "",
|
|
872
908
|
status: control?.status || "missing",
|
|
873
909
|
effectiveOn: control?.effectiveOn || null,
|
|
874
|
-
|
|
910
|
+
operationPattern: control?.operationPattern || "",
|
|
875
911
|
operationMode: control?.operationMode || "",
|
|
876
912
|
requirementIds: control?.requirementIds || [],
|
|
877
913
|
policyIds: control?.policyIds || [],
|
|
878
914
|
systemIds: control?.systemIds || [],
|
|
879
|
-
riskIds:
|
|
915
|
+
riskIds: [...byId.values()]
|
|
916
|
+
.filter((record) => record.type === "risk" && (record.controlIds || []).includes(controlId))
|
|
917
|
+
.map(({ id }) => id)
|
|
918
|
+
.sort(),
|
|
880
919
|
evidenceIds: [...linkedEvidenceIds].sort(),
|
|
881
|
-
operatingRecordIds: operatingRecords.map(({ id }) => id).sort(),
|
|
920
|
+
operatingRecordIds: [...operatingRecords, ...zeroPopulationRecords].map(({ id }) => id).sort(),
|
|
882
921
|
tests: tests.map((test) => ({
|
|
883
922
|
...testPopulationSummary(test, byId),
|
|
884
923
|
id: test.id,
|
|
885
924
|
status: test.status,
|
|
886
925
|
outcome: test.outcome || null,
|
|
887
|
-
|
|
888
|
-
periodEnd: test.periodEnd || null,
|
|
889
|
-
asOfDate: test.asOfDate || null,
|
|
926
|
+
coverage: test.coverage || null,
|
|
890
927
|
sampleSize: test.sampleSize ?? null,
|
|
891
928
|
sampleEvidenceIds: test.sampleEvidenceIds || [],
|
|
892
929
|
exceptionCount: test.exceptionCount ?? null
|
|
@@ -906,6 +943,17 @@ function controlIdsForRecord(record, byId, seen = new Set()) {
|
|
|
906
943
|
for (const sourceId of record.sourceResourceIds || []) addIds(ids, controlIdsForRecord(byId.get(sourceId), byId, seen));
|
|
907
944
|
if (record.sourceResourceId) addIds(ids, controlIdsForRecord(byId.get(record.sourceResourceId), byId, seen));
|
|
908
945
|
if (record.obligationId) addIds(ids, byId.get(record.obligationId)?.controlIds);
|
|
946
|
+
for (const candidate of byId.values()) {
|
|
947
|
+
if (
|
|
948
|
+
candidate.type === "obligation"
|
|
949
|
+
&& (candidate.completionResourceIds || []).includes(record.id)
|
|
950
|
+
) {
|
|
951
|
+
addIds(ids, candidate.controlIds);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
for (const subjectId of record.subjectResourceIds || []) {
|
|
955
|
+
addIds(ids, controlIdsForRecord(byId.get(subjectId), byId, seen));
|
|
956
|
+
}
|
|
909
957
|
return ids;
|
|
910
958
|
}
|
|
911
959
|
|
|
@@ -974,7 +1022,13 @@ function packetGaps({
|
|
|
974
1022
|
} else if (!controlNeedsExternalEvidence(control, model) && !coverage.operatingRecordIds.length) {
|
|
975
1023
|
gaps.push(gap("error", "control-missing-filegrc-evidence", `${coverage.code || coverage.title} has no dated filegrc operating record in the packet.`, coverage.id));
|
|
976
1024
|
}
|
|
977
|
-
if (
|
|
1025
|
+
if (
|
|
1026
|
+
audit?.auditKind !== "soc-2-type-1"
|
|
1027
|
+
&& coverage.status === "implemented"
|
|
1028
|
+
&& !coverage.operatingRecordIds.length
|
|
1029
|
+
&& !coverage.evidenceIds.length
|
|
1030
|
+
&& coverage.operationMode !== "automated"
|
|
1031
|
+
) {
|
|
978
1032
|
gaps.push(gap("warning", "control-missing-operating-record", `${coverage.code || coverage.title} has no dated operating record in the packet period.`, coverage.id));
|
|
979
1033
|
}
|
|
980
1034
|
for (const test of coverage.tests) {
|
|
@@ -1032,10 +1086,13 @@ function packetGaps({
|
|
|
1032
1086
|
for (const systemId of audit.systemIds || []) {
|
|
1033
1087
|
const system = byId.get(systemId);
|
|
1034
1088
|
if (!system) continue;
|
|
1035
|
-
const commitmentIds = new Set(
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1089
|
+
const commitmentIds = new Set(records
|
|
1090
|
+
.filter((record) => (
|
|
1091
|
+
record.type === "commitment"
|
|
1092
|
+
&& record.status === "active"
|
|
1093
|
+
&& (record.systemIds || []).includes(systemId)
|
|
1094
|
+
))
|
|
1095
|
+
.map(({ id }) => id));
|
|
1039
1096
|
if (!commitmentIds.size) {
|
|
1040
1097
|
gaps.push(gap("error", "system-missing-commitments", `${system.title} has no active service commitment or system requirement.`, system.id));
|
|
1041
1098
|
}
|
|
@@ -1043,8 +1100,8 @@ function packetGaps({
|
|
|
1043
1100
|
const recentAssessment = records.some((record) => (
|
|
1044
1101
|
record.type === "risk-assessment"
|
|
1045
1102
|
&& record.status === "complete"
|
|
1046
|
-
&& record.
|
|
1047
|
-
&& record.
|
|
1103
|
+
&& record.completedOn <= end
|
|
1104
|
+
&& record.completedOn >= shiftYear(end, -1)
|
|
1048
1105
|
&& (!(audit.systemIds || []).length || !(record.systemIds || []).length || record.systemIds.some((id) => audit.systemIds.includes(id)))
|
|
1049
1106
|
));
|
|
1050
1107
|
if (!recentAssessment) {
|
|
@@ -1058,10 +1115,10 @@ function packetGaps({
|
|
|
1058
1115
|
}
|
|
1059
1116
|
const reviews = records.filter((record) => (
|
|
1060
1117
|
record.type === "vendor-review"
|
|
1061
|
-
&&
|
|
1062
|
-
&&
|
|
1063
|
-
&& record.
|
|
1064
|
-
&& record.
|
|
1118
|
+
&& record.vendorId === vendorId
|
|
1119
|
+
&& record.status === "complete"
|
|
1120
|
+
&& record.completedOn <= end
|
|
1121
|
+
&& record.completedOn >= shiftYear(end, -1)
|
|
1065
1122
|
));
|
|
1066
1123
|
if (!reviews.length) {
|
|
1067
1124
|
gaps.push(gap("error", "missing-subservice-review", `${vendor.title} has no completed review in the year ending ${end}.`, vendor.id));
|
|
@@ -1073,7 +1130,10 @@ function packetGaps({
|
|
|
1073
1130
|
if (!vendorEvidence.length) {
|
|
1074
1131
|
gaps.push(gap("error", "missing-subservice-evidence", `${vendor.title} has no linked assurance evidence, such as its report and applicable bridge coverage.`, vendor.id));
|
|
1075
1132
|
} else {
|
|
1076
|
-
const assuranceReports = vendorEvidence.filter((item) =>
|
|
1133
|
+
const assuranceReports = vendorEvidence.filter((item) => (
|
|
1134
|
+
item.artifactKind === "third-party-report"
|
|
1135
|
+
|| /soc|assurance|vendor-report/i.test(item.artifactSubtype || "")
|
|
1136
|
+
));
|
|
1077
1137
|
if (!assuranceReports.length) {
|
|
1078
1138
|
gaps.push(gap("error", "missing-subservice-assurance-report", `${vendor.title} has linked evidence but no item identified as a SOC or other assurance report.`, vendor.id));
|
|
1079
1139
|
} else if (assuranceReports.every((item) => !item.periodEnd)) {
|
|
@@ -1081,7 +1141,7 @@ function packetGaps({
|
|
|
1081
1141
|
} else {
|
|
1082
1142
|
const latestCoverage = assuranceReports.map((item) => item.periodEnd).filter(Boolean).sort().at(-1);
|
|
1083
1143
|
const bridgeEvidence = vendorEvidence.some((item) => (
|
|
1084
|
-
/bridge/i.test(item.
|
|
1144
|
+
/bridge/i.test(item.artifactSubtype || "")
|
|
1085
1145
|
&& ((item.periodEnd && item.periodEnd >= end) || item.collectedOn >= end)
|
|
1086
1146
|
));
|
|
1087
1147
|
if (latestCoverage && latestCoverage < end && !bridgeEvidence) {
|
|
@@ -1116,6 +1176,13 @@ function packetGaps({
|
|
|
1116
1176
|
`${run.title}: ${action.title} was canceled. Complete the requirement or cancel the event with a documented reason.`,
|
|
1117
1177
|
action.actionItemId
|
|
1118
1178
|
));
|
|
1179
|
+
} else if (action.status === "blocked") {
|
|
1180
|
+
gaps.push(gap(
|
|
1181
|
+
"error",
|
|
1182
|
+
"blocked-event-action",
|
|
1183
|
+
`${run.title}: ${action.title} is blocked. ${action.blockingReason || "Open the Action Item and resolve its named blockers."}`,
|
|
1184
|
+
action.actionItemId
|
|
1185
|
+
));
|
|
1119
1186
|
} else if (action.missingCompletion) {
|
|
1120
1187
|
gaps.push(gap(
|
|
1121
1188
|
"error",
|
|
@@ -1130,7 +1197,7 @@ function packetGaps({
|
|
|
1130
1197
|
}
|
|
1131
1198
|
}
|
|
1132
1199
|
for (const item of evidence) {
|
|
1133
|
-
if (item.
|
|
1200
|
+
if (item.artifactKind === "rendered-page" && !item.sourceCommit) {
|
|
1134
1201
|
gaps.push(gap("error", "unbound-rendered-evidence", `${item.title} does not name the Git revision that was rendered.`, item.id));
|
|
1135
1202
|
} else if (item.sourceCommit && !item.sourceCommitValid) {
|
|
1136
1203
|
gaps.push(gap("error", "invalid-evidence-revision", `${item.title} names a source Git revision that is not available in this repository.`, item.id));
|
|
@@ -1164,21 +1231,21 @@ function packetGaps({
|
|
|
1164
1231
|
gaps.push(gap("warning", "evidence-source-role-missing", `${sourceSystem.title} has no evidence source role. Record what authoritative reports or records it supplies and keep extraction instructions in its Record Markdown.`, sourceSystem.id));
|
|
1165
1232
|
}
|
|
1166
1233
|
}
|
|
1167
|
-
} else if (["population-export", "system-export", "configuration-export"].includes(item.
|
|
1234
|
+
} else if (["population-export", "system-export", "configuration-export"].includes(item.artifactKind)) {
|
|
1168
1235
|
gaps.push(gap("error", "evidence-source-system-unrecorded", `${item.title} is a source-system export but does not link the cataloged system of record.`, item.id));
|
|
1169
1236
|
}
|
|
1170
1237
|
if (item.externalReference && !item.filePaths.length) {
|
|
1171
1238
|
gaps.push(gap("warning", "external-only-evidence", `${item.title} relies on an external reference and is not self-contained in the packet.`, item.id));
|
|
1172
1239
|
}
|
|
1173
|
-
if (item.
|
|
1240
|
+
if (item.artifactKind === "rendered-page") {
|
|
1174
1241
|
const captureComplete = item.capture
|
|
1175
1242
|
&& typeof item.capture.route === "string"
|
|
1176
1243
|
&& item.capture.route.trim()
|
|
1177
1244
|
&& item.capture.filters
|
|
1178
1245
|
&& typeof item.capture.filters === "object"
|
|
1179
1246
|
&& !Array.isArray(item.capture.filters)
|
|
1180
|
-
&&
|
|
1181
|
-
&&
|
|
1247
|
+
&& coverageStart(item.capture.coverage)
|
|
1248
|
+
&& coverageEnd(item.capture.coverage)
|
|
1182
1249
|
&& typeof item.capture.capturedAt === "string"
|
|
1183
1250
|
&& typeof item.capture.method === "string"
|
|
1184
1251
|
&& item.capture.method.trim();
|
|
@@ -1186,7 +1253,7 @@ function packetGaps({
|
|
|
1186
1253
|
gaps.push(gap("error", "missing-render-capture-context", `${item.title} does not record its route, filters, period, capture time, and method.`, item.id));
|
|
1187
1254
|
}
|
|
1188
1255
|
}
|
|
1189
|
-
if (item.
|
|
1256
|
+
if (item.artifactKind === "population-export") {
|
|
1190
1257
|
for (const [field, label] of [
|
|
1191
1258
|
["generatedAt", "generation time"],
|
|
1192
1259
|
["timezone", "report timezone"],
|
|
@@ -1217,22 +1284,23 @@ function controlNeedsExternalEvidence(control, model) {
|
|
|
1217
1284
|
const families = (model.evidenceSourceFamilies || []).filter((family) => (
|
|
1218
1285
|
(family.controlCodes || []).includes(control?.code)
|
|
1219
1286
|
));
|
|
1220
|
-
return !families.length || families.some((family) => family.
|
|
1287
|
+
return !families.length || families.some((family) => family.filegrcManaged !== true);
|
|
1221
1288
|
}
|
|
1222
1289
|
|
|
1223
1290
|
function auditGaps(gaps, audit, byId, records, start, end) {
|
|
1224
1291
|
if (!["soc-2-type-1", "soc-2-type-2"].includes(audit.auditKind)) {
|
|
1225
1292
|
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));
|
|
1226
1293
|
} else if (audit.auditKind === "soc-2-type-1") {
|
|
1227
|
-
|
|
1294
|
+
const asOf = coverageStart(audit.coverage);
|
|
1295
|
+
if (audit.coverage?.kind !== "as-of" || !asOf) {
|
|
1228
1296
|
gaps.push(gap("error", "audit-as-of-date-missing", `${audit.title} does not define a Type 1 as-of date.`, audit.id));
|
|
1229
|
-
} else if (start !==
|
|
1230
|
-
gaps.push(gap("error", "packet-date-mismatch", `Packet date ${start}${end !== start ? ` through ${end}` : ""} does not match the selected Type 1 as-of date ${
|
|
1297
|
+
} else if (start !== asOf || end !== asOf) {
|
|
1298
|
+
gaps.push(gap("error", "packet-date-mismatch", `Packet date ${start}${end !== start ? ` through ${end}` : ""} does not match the selected Type 1 as-of date ${asOf}.`, audit.id));
|
|
1231
1299
|
}
|
|
1232
|
-
} else if (
|
|
1300
|
+
} else if (audit.coverage?.kind !== "range") {
|
|
1233
1301
|
gaps.push(gap("error", "audit-period-missing", `${audit.title} does not define a Type 2 examination period.`, audit.id));
|
|
1234
|
-
} else if (audit.
|
|
1235
|
-
gaps.push(gap("error", "packet-period-mismatch", `Packet dates ${start} through ${end} do not match the selected audit period ${audit.
|
|
1302
|
+
} else if (!coverageMatches(audit.coverage, start, end)) {
|
|
1303
|
+
gaps.push(gap("error", "packet-period-mismatch", `Packet dates ${start} through ${end} do not match the selected audit period ${coverageStart(audit.coverage)} through ${coverageEnd(audit.coverage)}.`, audit.id));
|
|
1236
1304
|
}
|
|
1237
1305
|
if (!(audit.systemIds || []).length) gaps.push(gap("error", "audit-systems-missing", `${audit.title} has no in-scope systems.`, audit.id));
|
|
1238
1306
|
if (!(audit.requirementIds || []).length) gaps.push(gap("error", "audit-requirements-missing", `${audit.title} has no selected criteria.`, audit.id));
|
|
@@ -1246,7 +1314,7 @@ function auditGaps(gaps, audit, byId, records, start, end) {
|
|
|
1246
1314
|
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));
|
|
1247
1315
|
}
|
|
1248
1316
|
}
|
|
1249
|
-
if (!audit.
|
|
1317
|
+
if (!audit.auditorVendorId) gaps.push(gap("error", "auditor-missing", `${audit.title} does not identify the independent CPA firm.`, audit.id));
|
|
1250
1318
|
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));
|
|
1251
1319
|
if ((audit.subserviceVendorIds || []).length && audit.subserviceMethod === "not-applicable") {
|
|
1252
1320
|
gaps.push(gap("error", "subservice-scope-conflict", `${audit.title} names subservice organizations but marks their treatment not applicable.`, audit.id));
|
|
@@ -1347,12 +1415,15 @@ function evidenceSummary(record, byId, revisionIsValid) {
|
|
|
1347
1415
|
id: record.id,
|
|
1348
1416
|
title: record.title,
|
|
1349
1417
|
status: record.status,
|
|
1350
|
-
|
|
1351
|
-
|
|
1418
|
+
artifactKind: record.artifactKind,
|
|
1419
|
+
artifactSubtype: record.artifactSubtype || null,
|
|
1420
|
+
sourceKind: record.sourceKind,
|
|
1421
|
+
sourceDescription: record.sourceDescription,
|
|
1352
1422
|
collectedOn: record.collectedOn,
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1423
|
+
coverage: record.coverage || null,
|
|
1424
|
+
periodStart: coverageStart(record.coverage),
|
|
1425
|
+
periodEnd: coverageEnd(record.coverage),
|
|
1426
|
+
classificationId: record.classificationId,
|
|
1356
1427
|
generatedAt: record.generatedAt || null,
|
|
1357
1428
|
timezone: record.timezone || null,
|
|
1358
1429
|
queryDescription: record.queryDescription || null,
|
|
@@ -1384,13 +1455,14 @@ function populationSummary(record, byId) {
|
|
|
1384
1455
|
status: record.status,
|
|
1385
1456
|
auditId: record.auditId,
|
|
1386
1457
|
populationKind: record.populationKind,
|
|
1387
|
-
|
|
1388
|
-
|
|
1458
|
+
coverage: record.coverage || null,
|
|
1459
|
+
periodStart: coverageStart(record.coverage),
|
|
1460
|
+
periodEnd: coverageEnd(record.coverage),
|
|
1389
1461
|
controlIds: record.controlIds || [],
|
|
1390
1462
|
sourceSystemId,
|
|
1391
1463
|
sourceSystem: byId.get(sourceSystemId)?.title || null,
|
|
1392
1464
|
sourceEvidenceId: record.sourceEvidenceId || null,
|
|
1393
|
-
source: evidence?.
|
|
1465
|
+
source: evidence?.sourceDescription || null,
|
|
1394
1466
|
populationCount: evidence?.populationCount ?? null,
|
|
1395
1467
|
queryDescription: evidence?.queryDescription || null,
|
|
1396
1468
|
timezone: evidence?.timezone || null,
|
|
@@ -1439,7 +1511,7 @@ function populationGaps(gaps, audit, populations, byId, model) {
|
|
|
1439
1511
|
}
|
|
1440
1512
|
}
|
|
1441
1513
|
for (const population of populations) {
|
|
1442
|
-
if (population.
|
|
1514
|
+
if (!coverageMatches(population.coverage, coverageStart(audit.coverage), coverageEnd(audit.coverage))) {
|
|
1443
1515
|
gaps.push(gap("error", "population-period-mismatch", `${population.title} does not match the exact audit period.`, population.id));
|
|
1444
1516
|
}
|
|
1445
1517
|
if (population.status === "not-applicable") {
|
|
@@ -1462,11 +1534,11 @@ function populationGaps(gaps, audit, populations, byId, model) {
|
|
|
1462
1534
|
gaps.push(gap("error", "population-exceptions-undocumented", `${population.title} concludes with exceptions but does not describe them in the reconciliation summary.`, population.id));
|
|
1463
1535
|
}
|
|
1464
1536
|
const evidence = byId.get(population.sourceEvidenceId);
|
|
1465
|
-
if (!evidence || evidence.type !== "evidence" || evidence.
|
|
1537
|
+
if (!evidence || evidence.type !== "evidence" || evidence.artifactKind !== "population-export") {
|
|
1466
1538
|
gaps.push(gap("error", "population-export-missing", `${population.title} does not link a population-export evidence record.`, population.id));
|
|
1467
1539
|
continue;
|
|
1468
1540
|
}
|
|
1469
|
-
if (evidence.
|
|
1541
|
+
if (!coverageMatches(evidence.coverage, coverageStart(audit.coverage), coverageEnd(audit.coverage))) {
|
|
1470
1542
|
gaps.push(gap("error", "population-evidence-period-mismatch", `${evidence.title} does not cover the exact audit period.`, evidence.id));
|
|
1471
1543
|
}
|
|
1472
1544
|
const generatedOn = timestampDate(evidence.generatedAt, evidence.timezone);
|
|
@@ -1488,10 +1560,10 @@ function populationGaps(gaps, audit, populations, byId, model) {
|
|
|
1488
1560
|
|
|
1489
1561
|
function evidenceCoversPacketDate(item, audit, start, end) {
|
|
1490
1562
|
if (audit?.auditKind === "soc-2-type-1") {
|
|
1491
|
-
return (item.
|
|
1563
|
+
return coverageContains(item.coverage, start)
|
|
1492
1564
|
|| item.collectedOn === start;
|
|
1493
1565
|
}
|
|
1494
|
-
return (item.
|
|
1566
|
+
return coverageOverlaps(item.coverage, start, end)
|
|
1495
1567
|
|| (item.collectedOn >= start && item.collectedOn <= end);
|
|
1496
1568
|
}
|
|
1497
1569
|
|
|
@@ -1502,7 +1574,7 @@ function displaySourceKind(value) {
|
|
|
1502
1574
|
function overlapsEvidencePeriod(record, start, end) {
|
|
1503
1575
|
return record.type === "evidence" && (
|
|
1504
1576
|
(record.collectedOn >= start && record.collectedOn <= end)
|
|
1505
|
-
|| (record.
|
|
1577
|
+
|| coverageOverlaps(record.coverage, start, end)
|
|
1506
1578
|
);
|
|
1507
1579
|
}
|
|
1508
1580
|
|
|
@@ -1580,7 +1652,7 @@ function packetHtml(packet) {
|
|
|
1580
1652
|
? `<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>`
|
|
1581
1653
|
: "<p>No recurring occurrences intersect this period.</p>";
|
|
1582
1654
|
const evidence = packet.evidence.length
|
|
1583
|
-
? `<table><thead><tr><th>External Evidence</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.
|
|
1655
|
+
? `<table><thead><tr><th>External Evidence</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>`
|
|
1584
1656
|
: "<p>No External Evidence records were selected.</p>";
|
|
1585
1657
|
const sourceSystems = packet.sourceSystems.length
|
|
1586
1658
|
? `<p><a href="source-system-index.csv">Download source system index CSV</a></p><table><thead><tr><th>System of record</th><th>Evidence roles</th><th>Audit relationship</th><th>Evidence</th></tr></thead><tbody>${packet.sourceSystems.map((item) => `<tr><td><a href="records/system/${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 ? "In-scope system" : "Evidence source"}</td><td>${item.evidenceIds.length}</td></tr>`).join("")}</tbody></table><p><a href="external-evidence-index.csv">Download external evidence delivery index CSV</a></p>`
|
|
@@ -1669,11 +1741,11 @@ function requireDate(value, label) {
|
|
|
1669
1741
|
function resolvePacketPeriod(options, audit) {
|
|
1670
1742
|
const typeOne = audit?.auditKind === "soc-2-type-1";
|
|
1671
1743
|
const start = requireDate(
|
|
1672
|
-
options.start || (
|
|
1744
|
+
options.start || coverageStart(audit?.coverage),
|
|
1673
1745
|
typeOne ? "Type 1 as-of date" : "packet start date"
|
|
1674
1746
|
);
|
|
1675
1747
|
const end = requireDate(
|
|
1676
|
-
options.end || (
|
|
1748
|
+
options.end || coverageEnd(audit?.coverage),
|
|
1677
1749
|
typeOne ? "Type 1 as-of date" : "packet end date"
|
|
1678
1750
|
);
|
|
1679
1751
|
if (end < start) throw new Error("The packet end date must not be before its start date.");
|