filegrc 0.3.3 → 0.4.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 +17 -7
- package/model/index.js +37 -3
- package/model/v1.json +89 -52
- package/model/v2.json +8022 -0
- package/package.json +1 -1
- package/src/agent.js +36 -8
- package/src/audit-preparation.js +63 -60
- package/src/cli.js +176 -113
- package/src/coverage.js +50 -0
- package/src/evidence-packet.js +115 -75
- package/src/files.js +230 -28
- package/src/git-name.js +16 -0
- package/src/git.js +702 -6
- package/src/index.js +9 -6
- package/src/model-docs.js +88 -7
- package/src/model-migration.js +1463 -0
- package/src/mutation.js +46 -1
- package/src/obligations.js +108 -84
- package/src/parties.js +17 -2
- package/src/program-path.js +31 -58
- package/src/program-readiness.js +142 -106
- package/src/resource-status.js +17 -0
- package/src/server.js +175 -36
- package/src/setup.js +27 -28
- package/src/state.js +93 -27
- package/src/timing.js +41 -0
- package/src/validate.js +611 -43
- package/src/web.js +586 -141
- 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,8 +877,7 @@ 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)
|
|
@@ -871,12 +899,15 @@ function buildControlCoverage({ audit, byId, controlIds, evidenceIds, model, rec
|
|
|
871
899
|
activity: control?.activity || "",
|
|
872
900
|
status: control?.status || "missing",
|
|
873
901
|
effectiveOn: control?.effectiveOn || null,
|
|
874
|
-
|
|
902
|
+
operationPattern: control?.operationPattern || "",
|
|
875
903
|
operationMode: control?.operationMode || "",
|
|
876
904
|
requirementIds: control?.requirementIds || [],
|
|
877
905
|
policyIds: control?.policyIds || [],
|
|
878
906
|
systemIds: control?.systemIds || [],
|
|
879
|
-
riskIds:
|
|
907
|
+
riskIds: [...byId.values()]
|
|
908
|
+
.filter((record) => record.type === "risk" && (record.controlIds || []).includes(controlId))
|
|
909
|
+
.map(({ id }) => id)
|
|
910
|
+
.sort(),
|
|
880
911
|
evidenceIds: [...linkedEvidenceIds].sort(),
|
|
881
912
|
operatingRecordIds: operatingRecords.map(({ id }) => id).sort(),
|
|
882
913
|
tests: tests.map((test) => ({
|
|
@@ -884,9 +915,7 @@ function buildControlCoverage({ audit, byId, controlIds, evidenceIds, model, rec
|
|
|
884
915
|
id: test.id,
|
|
885
916
|
status: test.status,
|
|
886
917
|
outcome: test.outcome || null,
|
|
887
|
-
|
|
888
|
-
periodEnd: test.periodEnd || null,
|
|
889
|
-
asOfDate: test.asOfDate || null,
|
|
918
|
+
coverage: test.coverage || null,
|
|
890
919
|
sampleSize: test.sampleSize ?? null,
|
|
891
920
|
sampleEvidenceIds: test.sampleEvidenceIds || [],
|
|
892
921
|
exceptionCount: test.exceptionCount ?? null
|
|
@@ -1032,10 +1061,13 @@ function packetGaps({
|
|
|
1032
1061
|
for (const systemId of audit.systemIds || []) {
|
|
1033
1062
|
const system = byId.get(systemId);
|
|
1034
1063
|
if (!system) continue;
|
|
1035
|
-
const commitmentIds = new Set(
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1064
|
+
const commitmentIds = new Set(records
|
|
1065
|
+
.filter((record) => (
|
|
1066
|
+
record.type === "commitment"
|
|
1067
|
+
&& record.status === "active"
|
|
1068
|
+
&& (record.systemIds || []).includes(systemId)
|
|
1069
|
+
))
|
|
1070
|
+
.map(({ id }) => id));
|
|
1039
1071
|
if (!commitmentIds.size) {
|
|
1040
1072
|
gaps.push(gap("error", "system-missing-commitments", `${system.title} has no active service commitment or system requirement.`, system.id));
|
|
1041
1073
|
}
|
|
@@ -1043,8 +1075,8 @@ function packetGaps({
|
|
|
1043
1075
|
const recentAssessment = records.some((record) => (
|
|
1044
1076
|
record.type === "risk-assessment"
|
|
1045
1077
|
&& record.status === "complete"
|
|
1046
|
-
&& record.
|
|
1047
|
-
&& record.
|
|
1078
|
+
&& record.completedOn <= end
|
|
1079
|
+
&& record.completedOn >= shiftYear(end, -1)
|
|
1048
1080
|
&& (!(audit.systemIds || []).length || !(record.systemIds || []).length || record.systemIds.some((id) => audit.systemIds.includes(id)))
|
|
1049
1081
|
));
|
|
1050
1082
|
if (!recentAssessment) {
|
|
@@ -1058,10 +1090,10 @@ function packetGaps({
|
|
|
1058
1090
|
}
|
|
1059
1091
|
const reviews = records.filter((record) => (
|
|
1060
1092
|
record.type === "vendor-review"
|
|
1061
|
-
&&
|
|
1062
|
-
&&
|
|
1063
|
-
&& record.
|
|
1064
|
-
&& record.
|
|
1093
|
+
&& record.vendorId === vendorId
|
|
1094
|
+
&& record.status === "complete"
|
|
1095
|
+
&& record.completedOn <= end
|
|
1096
|
+
&& record.completedOn >= shiftYear(end, -1)
|
|
1065
1097
|
));
|
|
1066
1098
|
if (!reviews.length) {
|
|
1067
1099
|
gaps.push(gap("error", "missing-subservice-review", `${vendor.title} has no completed review in the year ending ${end}.`, vendor.id));
|
|
@@ -1073,7 +1105,10 @@ function packetGaps({
|
|
|
1073
1105
|
if (!vendorEvidence.length) {
|
|
1074
1106
|
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
1107
|
} else {
|
|
1076
|
-
const assuranceReports = vendorEvidence.filter((item) =>
|
|
1108
|
+
const assuranceReports = vendorEvidence.filter((item) => (
|
|
1109
|
+
item.artifactKind === "third-party-report"
|
|
1110
|
+
|| /soc|assurance|vendor-report/i.test(item.artifactSubtype || "")
|
|
1111
|
+
));
|
|
1077
1112
|
if (!assuranceReports.length) {
|
|
1078
1113
|
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
1114
|
} else if (assuranceReports.every((item) => !item.periodEnd)) {
|
|
@@ -1081,7 +1116,7 @@ function packetGaps({
|
|
|
1081
1116
|
} else {
|
|
1082
1117
|
const latestCoverage = assuranceReports.map((item) => item.periodEnd).filter(Boolean).sort().at(-1);
|
|
1083
1118
|
const bridgeEvidence = vendorEvidence.some((item) => (
|
|
1084
|
-
/bridge/i.test(item.
|
|
1119
|
+
/bridge/i.test(item.artifactSubtype || "")
|
|
1085
1120
|
&& ((item.periodEnd && item.periodEnd >= end) || item.collectedOn >= end)
|
|
1086
1121
|
));
|
|
1087
1122
|
if (latestCoverage && latestCoverage < end && !bridgeEvidence) {
|
|
@@ -1130,7 +1165,7 @@ function packetGaps({
|
|
|
1130
1165
|
}
|
|
1131
1166
|
}
|
|
1132
1167
|
for (const item of evidence) {
|
|
1133
|
-
if (item.
|
|
1168
|
+
if (item.artifactKind === "rendered-page" && !item.sourceCommit) {
|
|
1134
1169
|
gaps.push(gap("error", "unbound-rendered-evidence", `${item.title} does not name the Git revision that was rendered.`, item.id));
|
|
1135
1170
|
} else if (item.sourceCommit && !item.sourceCommitValid) {
|
|
1136
1171
|
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 +1199,21 @@ function packetGaps({
|
|
|
1164
1199
|
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
1200
|
}
|
|
1166
1201
|
}
|
|
1167
|
-
} else if (["population-export", "system-export", "configuration-export"].includes(item.
|
|
1202
|
+
} else if (["population-export", "system-export", "configuration-export"].includes(item.artifactKind)) {
|
|
1168
1203
|
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
1204
|
}
|
|
1170
1205
|
if (item.externalReference && !item.filePaths.length) {
|
|
1171
1206
|
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
1207
|
}
|
|
1173
|
-
if (item.
|
|
1208
|
+
if (item.artifactKind === "rendered-page") {
|
|
1174
1209
|
const captureComplete = item.capture
|
|
1175
1210
|
&& typeof item.capture.route === "string"
|
|
1176
1211
|
&& item.capture.route.trim()
|
|
1177
1212
|
&& item.capture.filters
|
|
1178
1213
|
&& typeof item.capture.filters === "object"
|
|
1179
1214
|
&& !Array.isArray(item.capture.filters)
|
|
1180
|
-
&&
|
|
1181
|
-
&&
|
|
1215
|
+
&& coverageStart(item.capture.coverage)
|
|
1216
|
+
&& coverageEnd(item.capture.coverage)
|
|
1182
1217
|
&& typeof item.capture.capturedAt === "string"
|
|
1183
1218
|
&& typeof item.capture.method === "string"
|
|
1184
1219
|
&& item.capture.method.trim();
|
|
@@ -1186,7 +1221,7 @@ function packetGaps({
|
|
|
1186
1221
|
gaps.push(gap("error", "missing-render-capture-context", `${item.title} does not record its route, filters, period, capture time, and method.`, item.id));
|
|
1187
1222
|
}
|
|
1188
1223
|
}
|
|
1189
|
-
if (item.
|
|
1224
|
+
if (item.artifactKind === "population-export") {
|
|
1190
1225
|
for (const [field, label] of [
|
|
1191
1226
|
["generatedAt", "generation time"],
|
|
1192
1227
|
["timezone", "report timezone"],
|
|
@@ -1217,22 +1252,23 @@ function controlNeedsExternalEvidence(control, model) {
|
|
|
1217
1252
|
const families = (model.evidenceSourceFamilies || []).filter((family) => (
|
|
1218
1253
|
(family.controlCodes || []).includes(control?.code)
|
|
1219
1254
|
));
|
|
1220
|
-
return !families.length || families.some((family) => family.
|
|
1255
|
+
return !families.length || families.some((family) => family.filegrcManaged !== true);
|
|
1221
1256
|
}
|
|
1222
1257
|
|
|
1223
1258
|
function auditGaps(gaps, audit, byId, records, start, end) {
|
|
1224
1259
|
if (!["soc-2-type-1", "soc-2-type-2"].includes(audit.auditKind)) {
|
|
1225
1260
|
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
1261
|
} else if (audit.auditKind === "soc-2-type-1") {
|
|
1227
|
-
|
|
1262
|
+
const asOf = coverageStart(audit.coverage);
|
|
1263
|
+
if (audit.coverage?.kind !== "as-of" || !asOf) {
|
|
1228
1264
|
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 ${
|
|
1265
|
+
} else if (start !== asOf || end !== asOf) {
|
|
1266
|
+
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
1267
|
}
|
|
1232
|
-
} else if (
|
|
1268
|
+
} else if (audit.coverage?.kind !== "range") {
|
|
1233
1269
|
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.
|
|
1270
|
+
} else if (!coverageMatches(audit.coverage, start, end)) {
|
|
1271
|
+
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
1272
|
}
|
|
1237
1273
|
if (!(audit.systemIds || []).length) gaps.push(gap("error", "audit-systems-missing", `${audit.title} has no in-scope systems.`, audit.id));
|
|
1238
1274
|
if (!(audit.requirementIds || []).length) gaps.push(gap("error", "audit-requirements-missing", `${audit.title} has no selected criteria.`, audit.id));
|
|
@@ -1246,7 +1282,7 @@ function auditGaps(gaps, audit, byId, records, start, end) {
|
|
|
1246
1282
|
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
1283
|
}
|
|
1248
1284
|
}
|
|
1249
|
-
if (!audit.
|
|
1285
|
+
if (!audit.auditorVendorId) gaps.push(gap("error", "auditor-missing", `${audit.title} does not identify the independent CPA firm.`, audit.id));
|
|
1250
1286
|
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
1287
|
if ((audit.subserviceVendorIds || []).length && audit.subserviceMethod === "not-applicable") {
|
|
1252
1288
|
gaps.push(gap("error", "subservice-scope-conflict", `${audit.title} names subservice organizations but marks their treatment not applicable.`, audit.id));
|
|
@@ -1347,12 +1383,15 @@ function evidenceSummary(record, byId, revisionIsValid) {
|
|
|
1347
1383
|
id: record.id,
|
|
1348
1384
|
title: record.title,
|
|
1349
1385
|
status: record.status,
|
|
1350
|
-
|
|
1351
|
-
|
|
1386
|
+
artifactKind: record.artifactKind,
|
|
1387
|
+
artifactSubtype: record.artifactSubtype || null,
|
|
1388
|
+
sourceKind: record.sourceKind,
|
|
1389
|
+
sourceDescription: record.sourceDescription,
|
|
1352
1390
|
collectedOn: record.collectedOn,
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1391
|
+
coverage: record.coverage || null,
|
|
1392
|
+
periodStart: coverageStart(record.coverage),
|
|
1393
|
+
periodEnd: coverageEnd(record.coverage),
|
|
1394
|
+
classificationId: record.classificationId,
|
|
1356
1395
|
generatedAt: record.generatedAt || null,
|
|
1357
1396
|
timezone: record.timezone || null,
|
|
1358
1397
|
queryDescription: record.queryDescription || null,
|
|
@@ -1384,13 +1423,14 @@ function populationSummary(record, byId) {
|
|
|
1384
1423
|
status: record.status,
|
|
1385
1424
|
auditId: record.auditId,
|
|
1386
1425
|
populationKind: record.populationKind,
|
|
1387
|
-
|
|
1388
|
-
|
|
1426
|
+
coverage: record.coverage || null,
|
|
1427
|
+
periodStart: coverageStart(record.coverage),
|
|
1428
|
+
periodEnd: coverageEnd(record.coverage),
|
|
1389
1429
|
controlIds: record.controlIds || [],
|
|
1390
1430
|
sourceSystemId,
|
|
1391
1431
|
sourceSystem: byId.get(sourceSystemId)?.title || null,
|
|
1392
1432
|
sourceEvidenceId: record.sourceEvidenceId || null,
|
|
1393
|
-
source: evidence?.
|
|
1433
|
+
source: evidence?.sourceDescription || null,
|
|
1394
1434
|
populationCount: evidence?.populationCount ?? null,
|
|
1395
1435
|
queryDescription: evidence?.queryDescription || null,
|
|
1396
1436
|
timezone: evidence?.timezone || null,
|
|
@@ -1439,7 +1479,7 @@ function populationGaps(gaps, audit, populations, byId, model) {
|
|
|
1439
1479
|
}
|
|
1440
1480
|
}
|
|
1441
1481
|
for (const population of populations) {
|
|
1442
|
-
if (population.
|
|
1482
|
+
if (!coverageMatches(population.coverage, coverageStart(audit.coverage), coverageEnd(audit.coverage))) {
|
|
1443
1483
|
gaps.push(gap("error", "population-period-mismatch", `${population.title} does not match the exact audit period.`, population.id));
|
|
1444
1484
|
}
|
|
1445
1485
|
if (population.status === "not-applicable") {
|
|
@@ -1462,11 +1502,11 @@ function populationGaps(gaps, audit, populations, byId, model) {
|
|
|
1462
1502
|
gaps.push(gap("error", "population-exceptions-undocumented", `${population.title} concludes with exceptions but does not describe them in the reconciliation summary.`, population.id));
|
|
1463
1503
|
}
|
|
1464
1504
|
const evidence = byId.get(population.sourceEvidenceId);
|
|
1465
|
-
if (!evidence || evidence.type !== "evidence" || evidence.
|
|
1505
|
+
if (!evidence || evidence.type !== "evidence" || evidence.artifactKind !== "population-export") {
|
|
1466
1506
|
gaps.push(gap("error", "population-export-missing", `${population.title} does not link a population-export evidence record.`, population.id));
|
|
1467
1507
|
continue;
|
|
1468
1508
|
}
|
|
1469
|
-
if (evidence.
|
|
1509
|
+
if (!coverageMatches(evidence.coverage, coverageStart(audit.coverage), coverageEnd(audit.coverage))) {
|
|
1470
1510
|
gaps.push(gap("error", "population-evidence-period-mismatch", `${evidence.title} does not cover the exact audit period.`, evidence.id));
|
|
1471
1511
|
}
|
|
1472
1512
|
const generatedOn = timestampDate(evidence.generatedAt, evidence.timezone);
|
|
@@ -1488,10 +1528,10 @@ function populationGaps(gaps, audit, populations, byId, model) {
|
|
|
1488
1528
|
|
|
1489
1529
|
function evidenceCoversPacketDate(item, audit, start, end) {
|
|
1490
1530
|
if (audit?.auditKind === "soc-2-type-1") {
|
|
1491
|
-
return (item.
|
|
1531
|
+
return coverageContains(item.coverage, start)
|
|
1492
1532
|
|| item.collectedOn === start;
|
|
1493
1533
|
}
|
|
1494
|
-
return (item.
|
|
1534
|
+
return coverageOverlaps(item.coverage, start, end)
|
|
1495
1535
|
|| (item.collectedOn >= start && item.collectedOn <= end);
|
|
1496
1536
|
}
|
|
1497
1537
|
|
|
@@ -1502,7 +1542,7 @@ function displaySourceKind(value) {
|
|
|
1502
1542
|
function overlapsEvidencePeriod(record, start, end) {
|
|
1503
1543
|
return record.type === "evidence" && (
|
|
1504
1544
|
(record.collectedOn >= start && record.collectedOn <= end)
|
|
1505
|
-
|| (record.
|
|
1545
|
+
|| coverageOverlaps(record.coverage, start, end)
|
|
1506
1546
|
);
|
|
1507
1547
|
}
|
|
1508
1548
|
|
|
@@ -1580,7 +1620,7 @@ function packetHtml(packet) {
|
|
|
1580
1620
|
? `<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
1621
|
: "<p>No recurring occurrences intersect this period.</p>";
|
|
1582
1622
|
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.
|
|
1623
|
+
? `<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
1624
|
: "<p>No External Evidence records were selected.</p>";
|
|
1585
1625
|
const sourceSystems = packet.sourceSystems.length
|
|
1586
1626
|
? `<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 +1709,11 @@ function requireDate(value, label) {
|
|
|
1669
1709
|
function resolvePacketPeriod(options, audit) {
|
|
1670
1710
|
const typeOne = audit?.auditKind === "soc-2-type-1";
|
|
1671
1711
|
const start = requireDate(
|
|
1672
|
-
options.start || (
|
|
1712
|
+
options.start || coverageStart(audit?.coverage),
|
|
1673
1713
|
typeOne ? "Type 1 as-of date" : "packet start date"
|
|
1674
1714
|
);
|
|
1675
1715
|
const end = requireDate(
|
|
1676
|
-
options.end || (
|
|
1716
|
+
options.end || coverageEnd(audit?.coverage),
|
|
1677
1717
|
typeOne ? "Type 1 as-of date" : "packet end date"
|
|
1678
1718
|
);
|
|
1679
1719
|
if (end < start) throw new Error("The packet end date must not be before its start date.");
|