filegrc 0.3.4 → 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 +16 -6
- package/model/index.js +37 -3
- package/model/v1.json +81 -47
- 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 +168 -110
- package/src/coverage.js +50 -0
- package/src/evidence-packet.js +115 -75
- package/src/files.js +230 -28
- package/src/git.js +239 -41
- package/src/index.js +5 -5
- package/src/model-docs.js +88 -7
- package/src/model-migration.js +1463 -0
- package/src/mutation.js +42 -0
- 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 +110 -39
- package/src/setup.js +27 -28
- package/src/state.js +86 -25
- package/src/timing.js +41 -0
- package/src/validate.js +609 -43
- package/src/web.js +506 -129
- package/src/workspace.js +15 -7
- package/src/evidence-tests.js +0 -69
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -66,6 +66,7 @@ export function buildAgentGuide(loaded, type, options = {}) {
|
|
|
66
66
|
const requiredAtCreation = fieldList.filter(({ required: isRequired }) => isRequired);
|
|
67
67
|
const conditionalRequirements = fieldList.filter(({ requiredWhen, required: isRequired }) => requiredWhen && !isRequired);
|
|
68
68
|
const optionalFields = fieldList.filter(({ required, requiredWhen }) => !required && !requiredWhen);
|
|
69
|
+
const recommendedMarkdown = markdown.filter(({ recommended }) => recommended);
|
|
69
70
|
const location = definition.singleton
|
|
70
71
|
? `data/${definition.singleton}`
|
|
71
72
|
: `data/${definition.collection}/${(definition.recordPath ?? "{id}.json").replaceAll("{id}", options.id || "{id}")}`;
|
|
@@ -91,15 +92,21 @@ export function buildAgentGuide(loaded, type, options = {}) {
|
|
|
91
92
|
markdown,
|
|
92
93
|
workflow: [
|
|
93
94
|
"Inspect existing records and relation candidates before writing.",
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
definition.singleton
|
|
96
|
+
? "Open the existing singleton record, then replace every null value and empty required array with facts from an authoritative source."
|
|
97
|
+
: "Create a scaffold, then replace every null value and empty required array with facts from an authoritative source.",
|
|
98
|
+
recommendedMarkdown.length
|
|
99
|
+
? "Keep model fields in JSON and use the recommended Markdown companion for the detailed work, decisions, results, exceptions, and follow-up that apply to this record."
|
|
100
|
+
: "Keep the current facts and lifecycle state in JSON. Add optional Record Markdown only when the model fields cannot explain the record clearly.",
|
|
96
101
|
"Run npx filegrc validate, review the full Git diff, and commit the JSON, Markdown, and attachments together with a message that explains why the record changed."
|
|
97
102
|
],
|
|
98
103
|
completionChecks: [
|
|
99
|
-
"
|
|
104
|
+
"Required and status-dependent fields are complete, and the lifecycle status matches the facts.",
|
|
100
105
|
"Every relationship resolves to the intended existing record.",
|
|
101
106
|
"Dates describe the business event in the workspace time zone, not the file edit time.",
|
|
102
|
-
|
|
107
|
+
...(recommendedMarkdown.length
|
|
108
|
+
? ["Required or recommended Markdown explains the work, decisions, results, exceptions, and follow-up that apply."]
|
|
109
|
+
: ["The structured fields state the current fact clearly; optional Record Markdown is added only when needed."]),
|
|
103
110
|
"No secrets or personal data that may need erasure were added to Git."
|
|
104
111
|
]
|
|
105
112
|
};
|
|
@@ -131,14 +138,22 @@ export function scaffoldResourceMutation(loaded, type, title, options = {}) {
|
|
|
131
138
|
...(definition.required ?? [])
|
|
132
139
|
]);
|
|
133
140
|
const record = {
|
|
134
|
-
schemaVersion: 1,
|
|
135
141
|
id,
|
|
136
142
|
type,
|
|
137
143
|
title: normalizedTitle
|
|
138
144
|
};
|
|
139
145
|
for (const name of required) {
|
|
140
146
|
if (record[name] !== undefined) continue;
|
|
141
|
-
record[name] = scaffoldValue(name, fields[name]);
|
|
147
|
+
record[name] = scaffoldValue(name, fields[name], loaded.model);
|
|
148
|
+
}
|
|
149
|
+
for (const [name, field] of Object.entries(fields)) {
|
|
150
|
+
if (
|
|
151
|
+
record[name] === undefined
|
|
152
|
+
&& field.requiredWhen
|
|
153
|
+
&& conditionMatches(record, field.requiredWhen)
|
|
154
|
+
) {
|
|
155
|
+
record[name] = scaffoldValue(name, field, loaded.model);
|
|
156
|
+
}
|
|
142
157
|
}
|
|
143
158
|
|
|
144
159
|
const slots = markdownEntries(loaded.model, record).filter((slot) => (
|
|
@@ -210,17 +225,30 @@ function allowedValues(model, field) {
|
|
|
210
225
|
return null;
|
|
211
226
|
}
|
|
212
227
|
|
|
213
|
-
function scaffoldValue(name, field = {}) {
|
|
228
|
+
function scaffoldValue(name, field = {}, model) {
|
|
214
229
|
if (field.const !== undefined) return field.const;
|
|
215
230
|
if (name === "status" && field.values) {
|
|
216
231
|
return STARTING_STATUS_ORDER.find((value) => field.values.includes(value)) ?? field.values[0] ?? null;
|
|
217
232
|
}
|
|
218
233
|
if (field.type === "array") return [];
|
|
219
|
-
if (field.type === "object")
|
|
234
|
+
if (field.type === "object") {
|
|
235
|
+
const schema = model?.objectTypes?.[field.objectType];
|
|
236
|
+
if (!schema) return {};
|
|
237
|
+
return Object.fromEntries((schema.required || []).map((propertyName) => [
|
|
238
|
+
propertyName,
|
|
239
|
+
scaffoldValue(propertyName, schema.properties?.[propertyName], model)
|
|
240
|
+
]));
|
|
241
|
+
}
|
|
220
242
|
if (field.type === "boolean") return false;
|
|
221
243
|
return null;
|
|
222
244
|
}
|
|
223
245
|
|
|
246
|
+
function conditionMatches(record, condition) {
|
|
247
|
+
return Object.entries(condition || {}).every(([name, expected]) => (
|
|
248
|
+
Array.isArray(expected) ? expected.includes(record[name]) : record[name] === expected
|
|
249
|
+
));
|
|
250
|
+
}
|
|
251
|
+
|
|
224
252
|
function markdownScaffold(title, type, slot) {
|
|
225
253
|
const heading = slot.label === "Record" ? title : `${title}: ${slot.label}`;
|
|
226
254
|
const sections = slot.name === "agenda"
|
package/src/audit-preparation.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
+
import {
|
|
4
|
+
coverageContains,
|
|
5
|
+
coverageEnd,
|
|
6
|
+
coverageLabel,
|
|
7
|
+
coverageMatches,
|
|
8
|
+
coverageOverlaps,
|
|
9
|
+
coverageStart
|
|
10
|
+
} from "./coverage.js";
|
|
3
11
|
import { createResource, createResources, deleteResource, updateResource } from "./files.js";
|
|
4
12
|
import { createResourceId } from "./id.js";
|
|
5
13
|
import { partiesIndependent } from "./parties.js";
|
|
@@ -85,9 +93,8 @@ export async function assessAuditPreparation(input, options = {}) {
|
|
|
85
93
|
counts,
|
|
86
94
|
canInitialize: Boolean(audit
|
|
87
95
|
&& ["soc-2-type-1", "soc-2-type-2"].includes(audit.auditKind)
|
|
88
|
-
&& (audit.
|
|
89
|
-
|
|
90
|
-
: audit.periodStart && audit.periodEnd)
|
|
96
|
+
&& coverageStart(audit.coverage)
|
|
97
|
+
&& coverageEnd(audit.coverage)
|
|
91
98
|
&& initializationNeeded(audit, records, loaded.model)),
|
|
92
99
|
stages
|
|
93
100
|
};
|
|
@@ -100,10 +107,10 @@ export async function prepareAuditWorkspace(input, options = {}) {
|
|
|
100
107
|
if (!["soc-2-type-1", "soc-2-type-2"].includes(audit.auditKind)) {
|
|
101
108
|
throw new Error("Audit preparation requires a SOC 2 Type 1 or Type 2 engagement.");
|
|
102
109
|
}
|
|
103
|
-
if (audit.auditKind === "soc-2-type-2" &&
|
|
110
|
+
if (audit.auditKind === "soc-2-type-2" && audit.coverage?.kind !== "range") {
|
|
104
111
|
throw new Error("Set the Type 2 audit period before initializing audit preparation.");
|
|
105
112
|
}
|
|
106
|
-
if (audit.auditKind === "soc-2-type-1" &&
|
|
113
|
+
if (audit.auditKind === "soc-2-type-1" && audit.coverage?.kind !== "as-of") {
|
|
107
114
|
throw new Error("Set the Type 1 as-of date before initializing audit preparation.");
|
|
108
115
|
}
|
|
109
116
|
|
|
@@ -167,15 +174,13 @@ export async function prepareAuditWorkspace(input, options = {}) {
|
|
|
167
174
|
(system.evidenceSourceKinds || []).includes(template.sourceKind)
|
|
168
175
|
));
|
|
169
176
|
return {
|
|
170
|
-
schemaVersion: 1,
|
|
171
177
|
id,
|
|
172
178
|
type: "audit-population",
|
|
173
179
|
title: template.title,
|
|
174
180
|
status: "planned",
|
|
175
181
|
auditId: audit.id,
|
|
176
182
|
populationKind: template.kind,
|
|
177
|
-
|
|
178
|
-
periodEnd: audit.periodEnd,
|
|
183
|
+
coverage: structuredClone(audit.coverage),
|
|
179
184
|
ownerIds: [...audit.ownerIds],
|
|
180
185
|
...(controlIds.length ? { controlIds } : {}),
|
|
181
186
|
...(matchingSources.length === 1 ? { sourceSystemId: matchingSources[0].id } : {}),
|
|
@@ -219,9 +224,9 @@ function scopeStage(audit, records, byId, programReadiness) {
|
|
|
219
224
|
}
|
|
220
225
|
|
|
221
226
|
const periodComplete = audit.auditKind === "soc-2-type-2"
|
|
222
|
-
? audit.
|
|
227
|
+
? audit.coverage?.kind === "range"
|
|
223
228
|
: audit.auditKind === "soc-2-type-1"
|
|
224
|
-
? audit.
|
|
229
|
+
? audit.coverage?.kind === "as-of"
|
|
225
230
|
: false;
|
|
226
231
|
items.push(item(
|
|
227
232
|
"period",
|
|
@@ -229,8 +234,8 @@ function scopeStage(audit, records, byId, programReadiness) {
|
|
|
229
234
|
"Set the auditor-agreed report type and date",
|
|
230
235
|
periodComplete
|
|
231
236
|
? audit.auditKind === "soc-2-type-2"
|
|
232
|
-
? `Auditor-agreed Type 2 period: ${audit.
|
|
233
|
-
: `Auditor-agreed Type 1 as-of date: ${audit.
|
|
237
|
+
? `Auditor-agreed Type 2 period: ${coverageLabel(audit.coverage)}.`
|
|
238
|
+
: `Auditor-agreed Type 1 as-of date: ${coverageLabel(audit.coverage)}.`
|
|
234
239
|
: audit.auditKind === "soc-2-type-1"
|
|
235
240
|
? "Set the Type 1 as-of date."
|
|
236
241
|
: audit.auditKind === "soc-2-type-2"
|
|
@@ -238,11 +243,9 @@ function scopeStage(audit, records, byId, programReadiness) {
|
|
|
238
243
|
: "Change this readiness record to a Type 1 or Type 2 engagement before planning the report.",
|
|
239
244
|
audit
|
|
240
245
|
));
|
|
241
|
-
if (audit.auditKind === "soc-2-type-2" && programReadiness.target.
|
|
242
|
-
const candidate =
|
|
243
|
-
|
|
244
|
-
.join(" through ");
|
|
245
|
-
const agreed = [audit.periodStart, audit.periodEnd].filter(Boolean).join(" through ");
|
|
246
|
+
if (audit.auditKind === "soc-2-type-2" && programReadiness.target.candidateCoverage) {
|
|
247
|
+
const candidate = coverageLabel(programReadiness.target.candidateCoverage);
|
|
248
|
+
const agreed = coverageLabel(audit.coverage);
|
|
246
249
|
items.push(item(
|
|
247
250
|
"candidate-period-comparison",
|
|
248
251
|
"info",
|
|
@@ -257,9 +260,8 @@ function scopeStage(audit, records, byId, programReadiness) {
|
|
|
257
260
|
const systems = (audit.systemIds || []).map((id) => byId.get(id)).filter(Boolean);
|
|
258
261
|
const completeSystems = systems.filter((system) => (
|
|
259
262
|
system.status === "active"
|
|
260
|
-
&& system.inScope === true
|
|
261
263
|
&& system.description
|
|
262
|
-
&& system.
|
|
264
|
+
&& system.classificationId
|
|
263
265
|
&& (system.ownerIds || []).length
|
|
264
266
|
));
|
|
265
267
|
items.push(item(
|
|
@@ -272,12 +274,10 @@ function scopeStage(audit, records, byId, programReadiness) {
|
|
|
272
274
|
systems[0] || { type: "system" }
|
|
273
275
|
));
|
|
274
276
|
|
|
275
|
-
const engagementStart = audit.
|
|
277
|
+
const engagementStart = coverageStart(audit.coverage);
|
|
276
278
|
const commitments = records.filter((record) => record.type === "commitment"
|
|
277
279
|
&& record.status === "active"
|
|
278
|
-
&& systems.some((system) => (
|
|
279
|
-
(system.commitmentIds || []).includes(record.id) || (record.systemIds || []).includes(system.id)
|
|
280
|
-
)));
|
|
280
|
+
&& systems.some((system) => (record.systemIds || []).includes(system.id)));
|
|
281
281
|
const completeCommitments = commitments.filter((commitment) => (
|
|
282
282
|
commitment.statement
|
|
283
283
|
&& (commitment.ownerIds || []).length
|
|
@@ -287,7 +287,7 @@ function scopeStage(audit, records, byId, programReadiness) {
|
|
|
287
287
|
&& (commitment.controlIds || []).length
|
|
288
288
|
));
|
|
289
289
|
const systemsWithoutCommitments = systems.filter((system) => !completeCommitments.some((commitment) => (
|
|
290
|
-
(
|
|
290
|
+
(commitment.systemIds || []).includes(system.id)
|
|
291
291
|
)));
|
|
292
292
|
items.push(item(
|
|
293
293
|
"commitments",
|
|
@@ -432,7 +432,7 @@ function engagementStage(audit, byId, programReadiness) {
|
|
|
432
432
|
]);
|
|
433
433
|
}
|
|
434
434
|
const auditor = audit.auditorVendorId ? byId.get(audit.auditorVendorId) : null;
|
|
435
|
-
const named = Boolean(auditor
|
|
435
|
+
const named = Boolean(auditor);
|
|
436
436
|
return stage("engagement", "Engage the Auditor", "Record the independent CPA firm and engagement contacts before treating the audit as active.", [
|
|
437
437
|
item(
|
|
438
438
|
"engagement-record",
|
|
@@ -446,7 +446,7 @@ function engagementStage(audit, byId, programReadiness) {
|
|
|
446
446
|
named ? "complete" : "action",
|
|
447
447
|
"Record the independent CPA firm",
|
|
448
448
|
named
|
|
449
|
-
? `${auditor
|
|
449
|
+
? `${auditor.title} is recorded for the engagement.`
|
|
450
450
|
: "Select the CPA firm and record it here. The independent management policy reviewer is a different role.",
|
|
451
451
|
audit
|
|
452
452
|
)
|
|
@@ -479,7 +479,7 @@ async function documentsStage(loaded, audit, byId) {
|
|
|
479
479
|
const document = audit?.[definition.field] ? byId.get(audit[definition.field]) : null;
|
|
480
480
|
const source = document ? await primaryMarkdown(loaded, document) : "";
|
|
481
481
|
const contentIssues = managementDocumentContentIssues(source, definition, audit);
|
|
482
|
-
const engagementEnd = audit?.
|
|
482
|
+
const engagementEnd = coverageEnd(audit?.coverage);
|
|
483
483
|
if (document?.approvedOn && engagementEnd && document.approvedOn < engagementEnd) {
|
|
484
484
|
contentIssues.push(`Approve the final document on or after the engagement ${audit.auditKind === "soc-2-type-1" ? "date" : "period end"}.`);
|
|
485
485
|
}
|
|
@@ -543,11 +543,11 @@ function evidenceStage(audit, records, byId, model) {
|
|
|
543
543
|
const evidence = records.filter((record) => record.type === "evidence");
|
|
544
544
|
const externalEvidence = evidence.filter((record) => (
|
|
545
545
|
record.status === "verified"
|
|
546
|
-
&& (record.
|
|
546
|
+
&& (record.artifactKind !== "rendered-page" || record.sourceCommit)
|
|
547
547
|
&& evidenceRelevantToAuditDate(record, audit)
|
|
548
548
|
));
|
|
549
|
-
const managedFamilies = (model.evidenceSourceFamilies || []).filter((family) => family.
|
|
550
|
-
const externalFamilies = (model.evidenceSourceFamilies || []).filter((family) => family.
|
|
549
|
+
const managedFamilies = (model.evidenceSourceFamilies || []).filter((family) => family.filegrcManaged === true);
|
|
550
|
+
const externalFamilies = (model.evidenceSourceFamilies || []).filter((family) => family.filegrcManaged !== true);
|
|
551
551
|
const evidenceFamiliesFor = (control) => (model.evidenceSourceFamilies || []).filter((family) => (
|
|
552
552
|
(family.controlCodes || []).includes(control.code)
|
|
553
553
|
));
|
|
@@ -574,7 +574,7 @@ function evidenceStage(audit, records, byId, model) {
|
|
|
574
574
|
managedControls.length && controlsWithFilegrcRecords.length === managedControls.length ? "complete" : managedControls.length ? "action" : "info",
|
|
575
575
|
"Review filegrc Evidence",
|
|
576
576
|
managedControls.length
|
|
577
|
-
? `${controlsWithFilegrcRecords.length} of ${managedControls.length} selected controls that use filegrc workflows have a dated operating record for the formal period. Complete each Step
|
|
577
|
+
? `${controlsWithFilegrcRecords.length} of ${managedControls.length} selected controls that use filegrc workflows have a dated operating record for the formal period. Complete each Step 4 record, link it to the control, and add results in its structured fields or Markdown.`
|
|
578
578
|
: "No selected controls use a dedicated filegrc operating record.",
|
|
579
579
|
filegrcRecords[0] || { type: managedFamilies[0]?.operationRecordTypes?.[0] || "control" }
|
|
580
580
|
),
|
|
@@ -600,7 +600,7 @@ function evidenceStage(audit, records, byId, model) {
|
|
|
600
600
|
));
|
|
601
601
|
continue;
|
|
602
602
|
}
|
|
603
|
-
if (source.
|
|
603
|
+
if (source.filegrcManaged === true) {
|
|
604
604
|
const sourceRecords = filegrcRecords.filter((record) => (
|
|
605
605
|
relevantControls.some((control) => controlIdsForRecord(record, byId).has(control.id))
|
|
606
606
|
));
|
|
@@ -613,7 +613,7 @@ function evidenceStage(audit, records, byId, model) {
|
|
|
613
613
|
source.title,
|
|
614
614
|
coveredControls.length === relevantControls.length
|
|
615
615
|
? `${sourceRecords.length} dated filegrc ${sourceRecords.length === 1 ? "record" : "records"} cover ${relevantControls.length} mapped controls. External artifacts needed to support those results are linked from the operating records.`
|
|
616
|
-
: `${coveredControls.length} of ${relevantControls.length} mapped controls have a dated ${source.operationRecordTypes.map(displayValue).join(" or ")} record for the formal period. Complete the Step
|
|
616
|
+
: `${coveredControls.length} of ${relevantControls.length} mapped controls have a dated ${source.operationRecordTypes.map(displayValue).join(" or ")} record for the formal period. Complete the Step 4 work and attach or reference any supporting external artifact on that record.`,
|
|
617
617
|
sourceRecords[0] || { type: source.operationRecordTypes[0] }
|
|
618
618
|
));
|
|
619
619
|
continue;
|
|
@@ -687,7 +687,11 @@ function auditorStage() {
|
|
|
687
687
|
|
|
688
688
|
function populationResult(population, audit, byId) {
|
|
689
689
|
if (!population) return { status: "action", message: "Initialize this population for the engagement." };
|
|
690
|
-
if (
|
|
690
|
+
if (!coverageMatches(
|
|
691
|
+
population.coverage,
|
|
692
|
+
coverageStart(audit?.coverage),
|
|
693
|
+
coverageEnd(audit?.coverage)
|
|
694
|
+
)) {
|
|
691
695
|
return { status: "action", message: "The population period does not match the exact audit period." };
|
|
692
696
|
}
|
|
693
697
|
if (population.status === "not-applicable") {
|
|
@@ -712,12 +716,15 @@ function populationResult(population, audit, byId) {
|
|
|
712
716
|
];
|
|
713
717
|
const evidenceComplete = evidence
|
|
714
718
|
&& evidence.type === "evidence"
|
|
715
|
-
&& evidence.
|
|
719
|
+
&& evidence.artifactKind === "population-export"
|
|
716
720
|
&& evidence.status === "verified"
|
|
717
721
|
&& population.sourceSystemId
|
|
718
722
|
&& evidence.sourceSystemId === population.sourceSystemId
|
|
719
|
-
&&
|
|
720
|
-
|
|
723
|
+
&& coverageMatches(
|
|
724
|
+
evidence.coverage,
|
|
725
|
+
coverageStart(audit.coverage),
|
|
726
|
+
coverageEnd(audit.coverage)
|
|
727
|
+
)
|
|
721
728
|
&& requiredEvidence.every((field) => evidence[field] !== undefined && evidence[field] !== null && evidence[field] !== "");
|
|
722
729
|
const reconciliationComplete = (population.reconciledByIds || []).length
|
|
723
730
|
&& population.reconciledOn
|
|
@@ -726,14 +733,14 @@ function populationResult(population, audit, byId) {
|
|
|
726
733
|
const generatedOn = timestampDate(evidence?.generatedAt, evidence?.timezone);
|
|
727
734
|
const sequenceComplete = Number.isInteger(evidence?.populationCount)
|
|
728
735
|
&& evidence.populationCount >= 0
|
|
729
|
-
&& generatedOn > audit.
|
|
736
|
+
&& generatedOn > coverageEnd(audit.coverage)
|
|
730
737
|
&& population.reconciledOn >= generatedOn;
|
|
731
738
|
if (!evidenceComplete || !reconciliationComplete || !sequenceComplete) {
|
|
732
739
|
return { status: "action", message: "Finish the reconciliation and link a verified population export with its exact query, timezone, count, completeness check, and accuracy check." };
|
|
733
740
|
}
|
|
734
741
|
return {
|
|
735
742
|
status: "complete",
|
|
736
|
-
message: `${evidence.populationCount} items reconciled from ${evidence.
|
|
743
|
+
message: `${evidence.populationCount} items reconciled from ${evidence.sourceDescription || "the authoritative source"}${population.conclusion === "complete-with-exceptions" ? " with documented exceptions" : ""}.`
|
|
737
744
|
};
|
|
738
745
|
}
|
|
739
746
|
|
|
@@ -792,31 +799,30 @@ function applicableManagementDocuments(audit, readiness) {
|
|
|
792
799
|
}
|
|
793
800
|
|
|
794
801
|
function evidenceOverlaps(record, start, end) {
|
|
795
|
-
return (record.
|
|
802
|
+
return coverageOverlaps(record.coverage, start, end)
|
|
796
803
|
|| (record.collectedOn && record.collectedOn >= start && record.collectedOn <= end);
|
|
797
804
|
}
|
|
798
805
|
|
|
799
806
|
function evidenceRelevantToAuditDate(record, audit) {
|
|
800
807
|
if (!audit) return false;
|
|
801
808
|
if (audit.auditKind === "soc-2-type-1") {
|
|
802
|
-
const date = audit.
|
|
809
|
+
const date = coverageStart(audit.coverage);
|
|
803
810
|
return Boolean(date) && (
|
|
804
|
-
(record.
|
|
811
|
+
coverageContains(record.coverage, date)
|
|
805
812
|
|| record.collectedOn === date
|
|
806
813
|
);
|
|
807
814
|
}
|
|
808
|
-
|
|
809
|
-
|
|
815
|
+
const start = coverageStart(audit.coverage);
|
|
816
|
+
const end = coverageEnd(audit.coverage);
|
|
817
|
+
return Boolean(start && end) && evidenceOverlaps(record, start, end);
|
|
810
818
|
}
|
|
811
819
|
|
|
812
820
|
function recordRelevantToAuditDate(record, audit, model) {
|
|
813
821
|
if (!audit) return false;
|
|
814
|
-
const start = audit.
|
|
815
|
-
const end = audit.
|
|
822
|
+
const start = coverageStart(audit.coverage);
|
|
823
|
+
const end = coverageEnd(audit.coverage);
|
|
816
824
|
if (!start || !end) return false;
|
|
817
|
-
if (record.
|
|
818
|
-
return true;
|
|
819
|
-
}
|
|
825
|
+
if (coverageOverlaps(record.coverage, start, end)) return true;
|
|
820
826
|
const definition = model.resources[record.type];
|
|
821
827
|
const fields = { ...model.commonFields, ...(definition?.fields || {}) };
|
|
822
828
|
return Object.entries(fields).some(([name, field]) => {
|
|
@@ -872,9 +878,7 @@ function managementDocumentContentIssues(source, definition, audit) {
|
|
|
872
878
|
));
|
|
873
879
|
if (missingHeadings.length) return [`Add the missing description sections: ${missingHeadings.join(", ")}.`];
|
|
874
880
|
if (definition.dateBinding === "engagement" && audit) {
|
|
875
|
-
const dates = audit.
|
|
876
|
-
? [audit.typeOneAsOf]
|
|
877
|
-
: [audit.periodStart, audit.periodEnd];
|
|
881
|
+
const dates = [coverageStart(audit.coverage), coverageEnd(audit.coverage)];
|
|
878
882
|
if (dates.some((date) => date && !source.includes(date))) {
|
|
879
883
|
return ["Name the exact engagement date or period in the document."];
|
|
880
884
|
}
|
|
@@ -923,16 +927,16 @@ function materializeManagementMarkdown(source, audit, records) {
|
|
|
923
927
|
.map(displayValue))]
|
|
924
928
|
.join(", ");
|
|
925
929
|
const period = audit.auditKind === "soc-2-type-1"
|
|
926
|
-
? audit.
|
|
927
|
-
: audit.
|
|
928
|
-
?
|
|
930
|
+
? coverageStart(audit.coverage) || "[as-of date]"
|
|
931
|
+
: coverageStart(audit.coverage) && coverageEnd(audit.coverage)
|
|
932
|
+
? coverageLabel(audit.coverage)
|
|
929
933
|
: "[start date] through [end date]";
|
|
930
934
|
return withoutDiscarded
|
|
931
935
|
.replaceAll(`<!-- ${keep}:start -->`, "")
|
|
932
936
|
.replaceAll(`<!-- ${keep}:end -->`, "")
|
|
933
|
-
.replaceAll("[as-of date]", audit.
|
|
934
|
-
.replaceAll("[start date]", audit.
|
|
935
|
-
.replaceAll("[end date]", audit.
|
|
937
|
+
.replaceAll("[as-of date]", coverageStart(audit.coverage) || "[as-of date]")
|
|
938
|
+
.replaceAll("[start date]", coverageStart(audit.coverage) || "[start date]")
|
|
939
|
+
.replaceAll("[end date]", coverageEnd(audit.coverage) || "[end date]")
|
|
936
940
|
.replaceAll("[engagement date or period]", period)
|
|
937
941
|
.replaceAll("[engagement scope]", audit.scope || "[engagement scope]")
|
|
938
942
|
.replaceAll("[in-scope systems]", systems || "[in-scope systems]")
|
|
@@ -953,8 +957,7 @@ function auditSummary(audit) {
|
|
|
953
957
|
title: audit.title,
|
|
954
958
|
status: audit.status,
|
|
955
959
|
kind: audit.auditKind,
|
|
956
|
-
|
|
957
|
-
periodEnd: audit.periodEnd || null
|
|
960
|
+
coverage: audit.coverage || null
|
|
958
961
|
};
|
|
959
962
|
}
|
|
960
963
|
|