filegrc 0.6.5 → 0.7.1
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 +8 -0
- package/model/v4.json +179 -78
- package/package.json +2 -2
- package/src/audit-preparation.js +465 -48
- package/src/audit-transition.js +5 -0
- package/src/batch-review.js +14 -5
- package/src/cli.js +107 -0
- package/src/content-readiness.js +11 -0
- package/src/evidence-packet.js +202 -36
- package/src/files.js +58 -3
- package/src/git.js +39 -7
- package/src/index.js +7 -0
- package/src/policy-activation.js +84 -0
- package/src/policy-library/information-security-policy-v2.md +290 -0
- package/src/policy-library.js +826 -0
- package/src/program-lifecycle.js +4 -0
- package/src/program-path.js +12 -11
- package/src/program-readiness.js +309 -42
- package/src/server.js +8 -0
- package/src/setup.js +18 -1
- package/src/soc2.js +227 -0
- package/src/state.js +8 -0
- package/src/validate.js +43 -17
- package/src/web.js +218 -25
- package/src/workflow.js +111 -41
package/src/workflow.js
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { assessAuditPreparation } from "./audit-preparation.js";
|
|
22
22
|
import { assessProgramReadiness } from "./program-readiness.js";
|
|
23
23
|
import { resolveProgram } from "./program.js";
|
|
24
|
+
import { signatoryAppointmentIssue, soc2ReportEvidenceIssue, subsequentEventsReviewIssue } from "./soc2.js";
|
|
24
25
|
import { planReconciliation } from "./reconciliation.js";
|
|
25
26
|
import { currentCalendarDate } from "./time.js";
|
|
26
27
|
import { measureTiming } from "./timing.js";
|
|
@@ -304,15 +305,24 @@ function programFindings(program) {
|
|
|
304
305
|
}
|
|
305
306
|
|
|
306
307
|
function auditFindings(preparation) {
|
|
307
|
-
return preparation.stages.flatMap((stage) => stage.items.map((item) =>
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
308
|
+
return preparation.stages.flatMap((stage) => stage.items.map((item) => {
|
|
309
|
+
const finding = normalizeFinding(
|
|
310
|
+
`audit.${preparation.audit?.id || "unscoped"}.${stage.id}.${item.id}`,
|
|
311
|
+
item,
|
|
312
|
+
{
|
|
313
|
+
assessment: stage.id === "auditor" ? "audit-closure" : "audit-readiness",
|
|
314
|
+
stage: stage.id,
|
|
315
|
+
auditId: preparation.audit?.id || null
|
|
316
|
+
}
|
|
317
|
+
);
|
|
318
|
+
if (
|
|
319
|
+
finding.state === "ready"
|
|
320
|
+
&& !finding.actions.length
|
|
321
|
+
&& finding.subject?.type === "audit"
|
|
322
|
+
&& preparation.audit
|
|
323
|
+
) finding.actions = [mutationAction(preparation.audit)];
|
|
324
|
+
return finding;
|
|
325
|
+
}));
|
|
316
326
|
}
|
|
317
327
|
|
|
318
328
|
function validationFindings(validation, loaded) {
|
|
@@ -872,6 +882,7 @@ async function assessPeriodHealth(loaded, options) {
|
|
|
872
882
|
function auditLifecycleFindings(loaded, audits, program) {
|
|
873
883
|
if (!["3", "4"].includes(String(loaded.model.modelVersion))) return [];
|
|
874
884
|
const target = program || resolveProgram(loaded);
|
|
885
|
+
const byId = new Map(loaded.resources.map((record) => [record.id, record]));
|
|
875
886
|
const findings = [];
|
|
876
887
|
for (const audit of audits) {
|
|
877
888
|
if (!(audit.controlIds || []).length) {
|
|
@@ -917,45 +928,54 @@ function auditLifecycleFindings(loaded, audits, program) {
|
|
|
917
928
|
));
|
|
918
929
|
}
|
|
919
930
|
}
|
|
920
|
-
if (!["planning", "draft"].includes(audit.status)) {
|
|
921
|
-
if (!audit.engagementTermsDocumentId) {
|
|
922
|
-
findings.push(auditLifecycleFinding(
|
|
923
|
-
audit,
|
|
924
|
-
"engagement-terms",
|
|
925
|
-
"audit-readiness",
|
|
926
|
-
"Link the engagement terms",
|
|
927
|
-
"Record the CPA firm's engagement terms without representing its professional judgments as management facts."
|
|
928
|
-
));
|
|
929
|
-
}
|
|
930
|
-
if (!(audit.managementAcknowledgedByIds || []).length || !audit.managementAcknowledgedOn) {
|
|
931
|
-
findings.push(auditLifecycleFinding(
|
|
932
|
-
audit,
|
|
933
|
-
"management-acknowledgement",
|
|
934
|
-
"audit-readiness",
|
|
935
|
-
"Record management acknowledgement",
|
|
936
|
-
"Name who acknowledged the engagement terms and the actual acknowledgement date."
|
|
937
|
-
));
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
931
|
const lateStage = ["report-draft", "issued", "delivered", "complete"].includes(audit.status);
|
|
941
|
-
|
|
932
|
+
const subsequentEventsIssue = lateStage ? subsequentEventsReviewIssue(audit) : null;
|
|
933
|
+
if (subsequentEventsIssue) {
|
|
942
934
|
findings.push(auditLifecycleFinding(
|
|
943
935
|
audit,
|
|
944
936
|
"subsequent-events",
|
|
945
937
|
"audit-readiness",
|
|
946
938
|
"Complete the subsequent-events review",
|
|
947
|
-
|
|
939
|
+
subsequentEventsIssue.message
|
|
948
940
|
));
|
|
949
941
|
}
|
|
950
|
-
|
|
942
|
+
const signatoryIssue = String(loaded.model.modelVersion) === "4" && lateStage
|
|
943
|
+
? signatoryAppointmentIssue(audit, byId)
|
|
944
|
+
: null;
|
|
945
|
+
if (signatoryIssue) {
|
|
946
|
+
findings.push(auditLifecycleFinding(
|
|
947
|
+
audit,
|
|
948
|
+
"signatory-authority",
|
|
949
|
+
"audit-readiness",
|
|
950
|
+
"Confirm authorized signatories",
|
|
951
|
+
signatoryIssue.message
|
|
952
|
+
));
|
|
953
|
+
}
|
|
954
|
+
const packetIssue = ["fieldwork", "report-draft", "issued", "delivered", "complete"].includes(audit.status)
|
|
955
|
+
? packetDeliveryIssue(audit.packetDelivery)
|
|
956
|
+
: null;
|
|
957
|
+
if (packetIssue) {
|
|
951
958
|
findings.push(auditLifecycleFinding(
|
|
952
959
|
audit,
|
|
953
960
|
"packet-delivery",
|
|
954
961
|
"delivery-readiness",
|
|
955
962
|
"Approve and record packet delivery",
|
|
956
|
-
|
|
963
|
+
packetIssue
|
|
957
964
|
));
|
|
958
965
|
}
|
|
966
|
+
if (["issued", "delivered", "complete"].includes(audit.status)) {
|
|
967
|
+
const reportEvidence = loaded.resources.find((record) => record.id === audit.reportEvidenceId);
|
|
968
|
+
const reportIssue = soc2ReportEvidenceIssue(reportEvidence, audit, loaded.model.modelVersion);
|
|
969
|
+
if (reportIssue) {
|
|
970
|
+
findings.push(auditLifecycleFinding(
|
|
971
|
+
audit,
|
|
972
|
+
"report-evidence",
|
|
973
|
+
"audit-closure",
|
|
974
|
+
"Link the exact issued SOC 2 report",
|
|
975
|
+
reportIssue.message
|
|
976
|
+
));
|
|
977
|
+
}
|
|
978
|
+
}
|
|
959
979
|
if (audit.status !== "complete") {
|
|
960
980
|
const nextStep = auditClosureNextStep(audit.status);
|
|
961
981
|
findings.push(auditLifecycleFinding(
|
|
@@ -991,10 +1011,8 @@ function auditLifecycleFindings(loaded, audits, program) {
|
|
|
991
1011
|
));
|
|
992
1012
|
}
|
|
993
1013
|
for (const [field, title, message] of [
|
|
994
|
-
["reportEvidenceId", "Link the issued report", "Link the exact issued report evidence and its coverage."],
|
|
995
1014
|
["retentionDecision", "Record the retention decision", "Record the approved retention and authorized distribution decision."],
|
|
996
|
-
["carryForwardActionIds", "Review carry-forward work", "Record the next-period actions, including an explicit empty list when no carry-forward work remains."]
|
|
997
|
-
["signatoryAppointmentIds", "Confirm authorized signatories", "Link active authority Appointments for management assertion and representation signers."]
|
|
1015
|
+
["carryForwardActionIds", "Review carry-forward work", "Record the next-period actions, including an explicit empty list when no carry-forward work remains."]
|
|
998
1016
|
]) {
|
|
999
1017
|
if (field === "carryForwardActionIds" ? Array.isArray(audit[field]) : present(audit[field])) continue;
|
|
1000
1018
|
findings.push(auditLifecycleFinding(audit, field, "audit-closure", title, message));
|
|
@@ -1003,6 +1021,41 @@ function auditLifecycleFindings(loaded, audits, program) {
|
|
|
1003
1021
|
return findings;
|
|
1004
1022
|
}
|
|
1005
1023
|
|
|
1024
|
+
export function packetDeliveryIssue(delivery) {
|
|
1025
|
+
if (!delivery) {
|
|
1026
|
+
return "Record the least-disclosure review, redaction decision, recipient, delivery system, exact packet revision and manifest, management approval, delivery date, and receipt.";
|
|
1027
|
+
}
|
|
1028
|
+
if (!(delivery.classificationReviewedByIds || []).length || !(delivery.approvedByIds || []).length) {
|
|
1029
|
+
return "Name the people who performed the least-disclosure review and approved the delivery.";
|
|
1030
|
+
}
|
|
1031
|
+
for (const [field, label] of [
|
|
1032
|
+
["redactionDecision", "redaction decision"],
|
|
1033
|
+
["recipient", "recipient"],
|
|
1034
|
+
["deliverySystem", "delivery system"],
|
|
1035
|
+
["packetCommit", "packet Git revision"],
|
|
1036
|
+
["manifestChecksum", "manifest checksum"],
|
|
1037
|
+
["receiptReference", "delivery receipt reference"]
|
|
1038
|
+
]) {
|
|
1039
|
+
if (!String(delivery[field] || "").trim()) return `Record the ${label} for the delivered packet.`;
|
|
1040
|
+
}
|
|
1041
|
+
if (!/^[a-f0-9]{40}$/i.test(delivery.packetCommit)) {
|
|
1042
|
+
return "Record the delivered packet's exact 40-character Git commit.";
|
|
1043
|
+
}
|
|
1044
|
+
if (!/^(?:sha256:)?[a-f0-9]{64}$/i.test(delivery.manifestChecksum)) {
|
|
1045
|
+
return "Record the packet manifest checksum as a SHA-256 digest.";
|
|
1046
|
+
}
|
|
1047
|
+
if (
|
|
1048
|
+
!delivery.classificationReviewedOn
|
|
1049
|
+
|| !delivery.approvedOn
|
|
1050
|
+
|| !delivery.deliveredOn
|
|
1051
|
+
|| delivery.classificationReviewedOn > delivery.approvedOn
|
|
1052
|
+
|| delivery.approvedOn > delivery.deliveredOn
|
|
1053
|
+
) {
|
|
1054
|
+
return "Record chronological least-disclosure review, management approval, and delivery dates.";
|
|
1055
|
+
}
|
|
1056
|
+
return null;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1006
1059
|
function normalizeFinding(code, item, context) {
|
|
1007
1060
|
const state = findingState(item.status);
|
|
1008
1061
|
const subject = item.resourceType
|
|
@@ -1185,6 +1238,9 @@ function buildAssessments({ program, audits, auditPreparations, obligationPlan,
|
|
|
1185
1238
|
&& audits.every((audit) => audit.status === "complete")
|
|
1186
1239
|
&& !findings.some((finding) => finding.assessment === "audit-closure" && blockingFinding(finding));
|
|
1187
1240
|
const deliveryFindingKeys = findingKeys(findings, "delivery-readiness");
|
|
1241
|
+
const policyActivations = program.policyActivations || [];
|
|
1242
|
+
const policiesOperating = policyActivations.length > 0
|
|
1243
|
+
&& policyActivations.every(({ state }) => state === "active-and-operating");
|
|
1188
1244
|
return {
|
|
1189
1245
|
structuralValidity: assessment(
|
|
1190
1246
|
validation.ok ? "complete" : "needs-work",
|
|
@@ -1203,6 +1259,24 @@ function buildAssessments({ program, audits, auditPreparations, obligationPlan,
|
|
|
1203
1259
|
evidenceReady ? "Evidence collection can begin." : "Evidence collection prerequisites remain.",
|
|
1204
1260
|
findingKeys(findings, "program-configuration", ({ key }) => key.startsWith("program."))
|
|
1205
1261
|
),
|
|
1262
|
+
policyActivation: {
|
|
1263
|
+
status: policiesOperating ? "complete" : policyActivations.length ? "needs-work" : "not-started",
|
|
1264
|
+
message: policiesOperating
|
|
1265
|
+
? "Every required Policy is active and operating."
|
|
1266
|
+
: policyActivations.length
|
|
1267
|
+
? "Review the per-Policy implementation gaps, select the approved Policies, and confirm the Step 3 activation cutover."
|
|
1268
|
+
: "Approve the required Policies before activation assessment begins.",
|
|
1269
|
+
findingKeys: findingKeys(findings, "program-configuration", ({ key }) => key.includes(".policy-activation-")),
|
|
1270
|
+
policies: policyActivations
|
|
1271
|
+
},
|
|
1272
|
+
policyLibraryReview: {
|
|
1273
|
+
status: program.policyLibraryProposals?.length ? "review" : "current",
|
|
1274
|
+
message: program.policyLibraryProposals?.length
|
|
1275
|
+
? "Optional starter-library review proposals are available. Existing content is unchanged."
|
|
1276
|
+
: "No starter-library review proposal applies.",
|
|
1277
|
+
findingKeys: [],
|
|
1278
|
+
proposals: program.policyLibraryProposals || []
|
|
1279
|
+
},
|
|
1206
1280
|
periodHealth: assessment(
|
|
1207
1281
|
!periodStarted ? "not-started" : periodHealthy ? "complete" : "at-risk",
|
|
1208
1282
|
!evidenceReady
|
|
@@ -1430,14 +1504,10 @@ function periodFinding(key, title, message, state, subject, actions, dependencie
|
|
|
1430
1504
|
|
|
1431
1505
|
function auditClosureNextStep(status) {
|
|
1432
1506
|
const steps = {
|
|
1433
|
-
|
|
1507
|
+
planned: {
|
|
1434
1508
|
title: "Confirm the engagement and start audit preparation",
|
|
1435
1509
|
message: "Link the agreed engagement terms, confirm scope and management acknowledgement, then move the audit to in progress."
|
|
1436
1510
|
},
|
|
1437
|
-
draft: {
|
|
1438
|
-
title: "Finalize the draft engagement",
|
|
1439
|
-
message: "Resolve the draft scope and ownership, link the agreed engagement terms, then move the audit to in progress."
|
|
1440
|
-
},
|
|
1441
1511
|
"in-progress": {
|
|
1442
1512
|
title: "Begin fieldwork",
|
|
1443
1513
|
message: "Finish management preparation, confirm the fieldwork dates, and move the audit to fieldwork when the CPA firm begins testing."
|